From f45f52ea0d19c11413956f325387dc5d59472588 Mon Sep 17 00:00:00 2001 From: Titouan-joseph Cicorella Date: Tue, 12 Nov 2024 15:14:17 +0100 Subject: [PATCH 01/32] docs(v2): fix duplicate section of user, session, oidc and settings services (#8889) # Which Problems Are Solved Duplicate section in the doc ![image](https://github.com/user-attachments/assets/b9d31f87-9158-443f-8f76-1bae31fb7ee8) # How the Problems Are Solved Change the category link source to add a introduction section ![image](https://github.com/user-attachments/assets/562843e6-e8b9-4125-a3f7-8e4d2a24522d) --- docs/docusaurus.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c0c7d5a45c..f5f349d951 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -289,7 +289,7 @@ module.exports = { outputDir: "docs/apis/resources/user_service_v2", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "tag", + categoryLinkSource: "auto", }, }, session_v2: { @@ -297,7 +297,7 @@ module.exports = { outputDir: "docs/apis/resources/session_service_v2", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "tag", + categoryLinkSource: "auto", }, }, oidc_v2: { @@ -305,7 +305,7 @@ module.exports = { outputDir: "docs/apis/resources/oidc_service_v2", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "tag", + categoryLinkSource: "auto", }, }, settings_v2: { @@ -313,7 +313,7 @@ module.exports = { outputDir: "docs/apis/resources/settings_service_v2", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "tag", + categoryLinkSource: "auto", }, }, user_schema_v3: { From 69e9926bcc9f766aef5cb34babd94ef627337835 Mon Sep 17 00:00:00 2001 From: chuangjinglu Date: Tue, 12 Nov 2024 22:41:18 +0800 Subject: [PATCH 02/32] fix: fix slice init length (#8707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved The intention here should be to initialize a slice with a capacity of len(queriedOrgs.Orgs) rather than initializing the length of this slice. the online demo: https://go.dev/play/p/vNUPNjdb2gJ # How the Problems Are Solved use `processedOrgs := make([]string, 0, len(queriedOrgs.Orgs))` # Additional Changes None # Additional Context None Co-authored-by: Tim Möhlmann --- internal/api/grpc/admin/export.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 408a1a59fe..68b6053c2c 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -42,7 +42,7 @@ func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest } orgs := make([]*admin_pb.DataOrg, len(queriedOrgs.Orgs)) - processedOrgs := make([]string, len(queriedOrgs.Orgs)) + processedOrgs := make([]string, 0, len(queriedOrgs.Orgs)) processedProjects := make([]string, 0) processedGrants := make([]string, 0) processedUsers := make([]string, 0) From 778b4041ca6f0cdb2957081a0cb95cd92426c8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 12 Nov 2024 17:20:48 +0200 Subject: [PATCH 03/32] fix(oidc): do not return access token for response type id_token (#8777) # Which Problems Are Solved Do not return an access token for implicit flow from v1 login, if the `response_type` is `id_token` # How the Problems Are Solved Do not create the access token event if if the `response_type` is `id_token`. # Additional Changes Token endpoint calls without auth request, such as machine users, token exchange and refresh token, do not have a `response_type`. For such calls the `OIDCResponseTypeUnspecified` enum is added at a `-1` offset, in order not to break existing client configs. # Additional Context - https://discord.com/channels/927474939156643850/1294001717725237298 - Fixes https://github.com/zitadel/zitadel/issues/8776 --- internal/api/oidc/auth_request.go | 1 + internal/api/oidc/token_client_credentials.go | 1 + internal/api/oidc/token_code.go | 1 + internal/api/oidc/token_exchange.go | 2 + internal/api/oidc/token_jwt_profile.go | 1 + internal/api/oidc/token_refresh.go | 1 + internal/command/oidc_session.go | 7 +- internal/command/oidc_session_test.go | 91 +++++++++++++++++++ internal/domain/application_oidc.go | 3 +- 9 files changed, 105 insertions(+), 3 deletions(-) diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 173585ff13..138035af58 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -600,6 +600,7 @@ func (s *Server) authResponseToken(authReq *AuthRequest, authorizer op.Authorize nil, slices.Contains(scope, oidc.ScopeOfflineAccess), authReq.SessionID, + authReq.oidc().ResponseType, ) if err != nil { op.AuthRequestError(w, r, authReq, err, authorizer) diff --git a/internal/api/oidc/token_client_credentials.go b/internal/api/oidc/token_client_credentials.go index 0b836a03cc..5871e2f130 100644 --- a/internal/api/oidc/token_client_credentials.go +++ b/internal/api/oidc/token_client_credentials.go @@ -47,6 +47,7 @@ func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequ nil, false, "", + domain.OIDCResponseTypeUnspecified, ) if err != nil { return nil, err diff --git a/internal/api/oidc/token_code.go b/internal/api/oidc/token_code.go index 3aa53e629e..ee3585be69 100644 --- a/internal/api/oidc/token_code.go +++ b/internal/api/oidc/token_code.go @@ -87,6 +87,7 @@ func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.A nil, slices.Contains(scope, oidc.ScopeOfflineAccess), authReq.SessionID, + authReq.oidc().ResponseType, ) if err != nil { return nil, err diff --git a/internal/api/oidc/token_exchange.go b/internal/api/oidc/token_exchange.go index 63a594b940..3887ff7c51 100644 --- a/internal/api/oidc/token_exchange.go +++ b/internal/api/oidc/token_exchange.go @@ -300,6 +300,7 @@ func (s *Server) createExchangeAccessToken( actor, slices.Contains(scope, oidc.ScopeOfflineAccess), "", + domain.OIDCResponseTypeUnspecified, ) if err != nil { return "", "", "", 0, err @@ -346,6 +347,7 @@ func (s *Server) createExchangeJWT( actor, slices.Contains(scope, oidc.ScopeOfflineAccess), "", + domain.OIDCResponseTypeUnspecified, ) accessToken, err = s.createJWT(ctx, client, session, getUserInfo, roleAssertion, getSigner) if err != nil { diff --git a/internal/api/oidc/token_jwt_profile.go b/internal/api/oidc/token_jwt_profile.go index 4717d29f9c..d60e6a283e 100644 --- a/internal/api/oidc/token_jwt_profile.go +++ b/internal/api/oidc/token_jwt_profile.go @@ -57,6 +57,7 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr nil, false, "", + domain.OIDCResponseTypeUnspecified, ) if err != nil { return nil, err diff --git a/internal/api/oidc/token_refresh.go b/internal/api/oidc/token_refresh.go index f0d92fa521..76e85a4888 100644 --- a/internal/api/oidc/token_refresh.go +++ b/internal/api/oidc/token_refresh.go @@ -69,6 +69,7 @@ func (s *Server) refreshTokenV1(ctx context.Context, client *Client, r *op.Clien refreshToken.Actor, true, "", + domain.OIDCResponseTypeUnspecified, ) if err != nil { return nil, err diff --git a/internal/command/oidc_session.go b/internal/command/oidc_session.go index c2922f5194..bea17986ea 100644 --- a/internal/command/oidc_session.go +++ b/internal/command/oidc_session.go @@ -147,6 +147,7 @@ func (c *Commands) CreateOIDCSession(ctx context.Context, actor *domain.TokenActor, needRefreshToken bool, sessionID string, + responseType domain.OIDCResponseType, ) (session *OIDCSession, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -164,8 +165,10 @@ func (c *Commands) CreateOIDCSession(ctx context.Context, cmd.AddSession(ctx, userID, resourceOwner, sessionID, clientID, audience, scope, authMethods, authTime, nonce, preferredLanguage, userAgent) cmd.RegisterLogout(ctx, sessionID, userID, clientID, backChannelLogoutURI) - if err = cmd.AddAccessToken(ctx, scope, userID, resourceOwner, reason, actor); err != nil { - return nil, err + if responseType != domain.OIDCResponseTypeIDToken { + if err = cmd.AddAccessToken(ctx, scope, userID, resourceOwner, reason, actor); err != nil { + return nil, err + } } if needRefreshToken { if err = cmd.AddRefreshToken(ctx, userID); err != nil { diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index 43ca622a29..18a115eb00 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -749,6 +749,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { actor *domain.TokenActor needRefreshToken bool sessionID string + responseType domain.OIDCResponseType } tests := []struct { name string @@ -788,6 +789,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { Issuer: "foo.com", }, needRefreshToken: false, + responseType: domain.OIDCResponseTypeUnspecified, }, wantErr: io.ErrClosedPipe, }, @@ -844,6 +846,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { Issuer: "foo.com", }, needRefreshToken: false, + responseType: domain.OIDCResponseTypeUnspecified, }, wantErr: zerrors.ThrowPreconditionFailed(nil, "OIDCS-kj3g2", "Errors.User.NotActive"), }, @@ -918,6 +921,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { Issuer: "foo.com", }, needRefreshToken: false, + responseType: domain.OIDCResponseTypeUnspecified, }, want: &OIDCSession{ TokenID: "V2_oidcSessionID-at_accessTokenID", @@ -943,6 +947,87 @@ func TestCommands_CreateOIDCSession(t *testing.T) { }, }, }, + { + name: "ID token only", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + expectFilter(), // token lifetime + expectPush( + oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "userID", "org1", "", "clientID", []string{"audience"}, []string{"openid", "offline_access"}, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + ), + idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID"), + defaultAccessTokenLifetime: time.Hour, + defaultRefreshTokenLifetime: 7 * 24 * time.Hour, + defaultRefreshTokenIdleLifetime: 24 * time.Hour, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + userID: "userID", + resourceOwner: "org1", + clientID: "clientID", + audience: []string{"audience"}, + scope: []string{"openid", "offline_access"}, + authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + authTime: testNow, + nonce: "nonce", + preferredLanguage: &language.Afrikaans, + userAgent: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + reason: domain.TokenReasonAuthRequest, + actor: &domain.TokenActor{ + UserID: "user2", + Issuer: "foo.com", + }, + needRefreshToken: false, + responseType: domain.OIDCResponseTypeIDToken, + }, + want: &OIDCSession{ + ClientID: "clientID", + UserID: "userID", + Audience: []string{"audience"}, + Scope: []string{"openid", "offline_access"}, + AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + AuthTime: testNow, + Nonce: "nonce", + PreferredLanguage: &language.Afrikaans, + UserAgent: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + }, + }, { name: "disable user token event", fields: fields{ @@ -1018,6 +1103,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { Issuer: "foo.com", }, needRefreshToken: false, + responseType: domain.OIDCResponseTypeUnspecified, }, want: &OIDCSession{ TokenID: "V2_oidcSessionID-at_accessTokenID", @@ -1115,6 +1201,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { Issuer: "foo.com", }, needRefreshToken: true, + responseType: domain.OIDCResponseTypeUnspecified, }, want: &OIDCSession{ TokenID: "V2_oidcSessionID-at_accessTokenID", @@ -1213,6 +1300,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { }, needRefreshToken: false, sessionID: "sessionID", + responseType: domain.OIDCResponseTypeUnspecified, }, want: &OIDCSession{ TokenID: "V2_oidcSessionID-at_accessTokenID", @@ -1594,6 +1682,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { Issuer: "foo.com", }, needRefreshToken: false, + responseType: domain.OIDCResponseTypeUnspecified, }, wantErr: zerrors.ThrowPermissionDenied(nil, "test", "test"), }, @@ -1675,6 +1764,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { Issuer: "foo.com", }, needRefreshToken: false, + responseType: domain.OIDCResponseTypeUnspecified, }, want: &OIDCSession{ TokenID: "V2_oidcSessionID-at_accessTokenID", @@ -1729,6 +1819,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { tt.args.actor, tt.args.needRefreshToken, tt.args.sessionID, + tt.args.responseType, ) require.ErrorIs(t, err, tt.wantErr) if got != nil { diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go index 1ffb61f538..617b889561 100644 --- a/internal/domain/application_oidc.go +++ b/internal/domain/application_oidc.go @@ -79,7 +79,8 @@ const ( type OIDCResponseType int32 const ( - OIDCResponseTypeCode OIDCResponseType = iota + OIDCResponseTypeUnspecified OIDCResponseType = iota - 1 // Negative offset not to break existing configs. + OIDCResponseTypeCode OIDCResponseTypeIDToken OIDCResponseTypeIDTokenToken ) From a09c772b034c5490249d8d61dd7ed4ea73c72f50 Mon Sep 17 00:00:00 2001 From: chris-1o Date: Tue, 12 Nov 2024 17:03:41 +0100 Subject: [PATCH 04/32] fix(mirror): Fix instance_id check for tables without (#8852) # Which Problems Are Solved Fixes 'column "instance_id" does not exist' errors from #8558. # How the Problems Are Solved The instanceClause / WHERE clause in the query for the respective tables is excluded. I have successfully created a mirror with this change. --- cmd/mirror/verify.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd/mirror/verify.go b/cmd/mirror/verify.go index 7b90ad89aa..68c927d091 100644 --- a/cmd/mirror/verify.go +++ b/cmd/mirror/verify.go @@ -5,13 +5,16 @@ import ( "database/sql" _ "embed" "fmt" + "slices" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" + cryptoDatabase "github.com/zitadel/zitadel/internal/crypto/database" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database/dialect" + "github.com/zitadel/zitadel/internal/query/projection" ) func verifyCmd() *cobra.Command { @@ -98,12 +101,22 @@ func getViews(ctx context.Context, dest *database.DB, schema string) (tables []s } func countEntries(ctx context.Context, client *database.DB, table string) (count int) { + instanceClause := instanceClause() + noInstanceIDColumn := []string{ + projection.InstanceProjectionTable, + projection.SystemFeatureTable, + cryptoDatabase.EncryptionKeysTable, + } + if slices.Contains(noInstanceIDColumn, table) { + instanceClause = "" + } + err := client.QueryRowContext( ctx, func(r *sql.Row) error { return r.Scan(&count) }, - fmt.Sprintf("SELECT COUNT(*) FROM %s %s", table, instanceClause()), + fmt.Sprintf("SELECT COUNT(*) FROM %s %s", table, instanceClause), ) logging.WithFields("table", table, "db", client.DatabaseName()).OnError(err).Error("unable to count") From ecbf0db15b6bd09587bdaebc2874ec7c207864ea Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 13 Nov 2024 08:50:23 +0100 Subject: [PATCH 05/32] fix(setup): improve search query to use index (#8898) # Which Problems Are Solved The setup filter for previous steps and kept getting slower. This is due to the filter, which did not provide any instanceID and thus resulting in a full table scan. # How the Problems Are Solved - Added an empty instanceID filter (since it's on system level) # Additional Changes None # Additional Context Noticed internally and during migrations on some regions --- internal/migration/step.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/migration/step.go b/internal/migration/step.go index 036a0a1055..04cae07e32 100644 --- a/internal/migration/step.go +++ b/internal/migration/step.go @@ -18,6 +18,7 @@ type StepStates struct { // Query implements eventstore.QueryReducer. func (*StepStates) Query() *eventstore.SearchQueryBuilder { return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + InstanceID(""). // to make sure we can use an appropriate index AddQuery(). AggregateTypes(SystemAggregate). AggregateIDs(SystemAggregateID). From 3b7b0c69e6d064f9b34939f36c6878ae40df056e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 13 Nov 2024 20:11:48 +0200 Subject: [PATCH 06/32] feat(cache): redis circuit breaker (#8890) # Which Problems Are Solved If a redis cache has connection issues or any other type of permament error, it tanks the responsiveness of ZITADEL. We currently do not support things like Redis cluster or sentinel. So adding a simple redis cache improves performance but introduces a single point of failure. # How the Problems Are Solved Implement a [circuit breaker](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/dn589784(v=pandp.10)?redirectedfrom=MSDN) as [`redis.Limiter`](https://pkg.go.dev/github.com/redis/go-redis/v9#Limiter) by wrapping sony's [gobreaker](https://github.com/sony/gobreaker) package. This package is picked as it seems well maintained and we already use their `sonyflake` package # Additional Changes - The unit tests constructed an unused `redis.Client` and didn't cleanup the connector. This is now fixed. # Additional Context Closes #8864 --- cmd/defaults.yaml | 13 ++ go.mod | 1 + go.sum | 2 + .../cache/connector/redis/circuit_breaker.go | 90 ++++++++++ .../connector/redis/circuit_breaker_test.go | 168 ++++++++++++++++++ internal/cache/connector/redis/connector.go | 3 + internal/cache/connector/redis/redis_test.go | 31 ++-- 7 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 internal/cache/connector/redis/circuit_breaker.go create mode 100644 internal/cache/connector/redis/circuit_breaker_test.go diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 18f814a6d2..a188d1446b 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -291,6 +291,19 @@ Caches: DisableIndentity: false # Add suffix to client name. Default is empty. IdentitySuffix: "" + # Implementation of [Circuit Breaker Pattern](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/dn589784(v=pandp.10)?redirectedfrom=MSDN) + CircuitBreaker: + # Interval when the counters are reset to 0. + # 0 interval never resets the counters until the CB is opened. + Interval: 0 + # Amount of consecutive failures permitted + MaxConsecutiveFailures: 5 + # The ratio of failed requests out of total requests + MaxFailureRatio: 0.1 + # Timeout after opening of the CB, until the state is set to half-open. + Timeout: 60s + # The allowed amount of requests that are allowed to pass when the CB is half-open. + MaxRetryRequests: 1 # Instance caches auth middleware instances, gettable by domain or ID. Instance: diff --git a/go.mod b/go.mod index cf4e755605..2928d4dbfb 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/redis/go-redis/v9 v9.7.0 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/sony/gobreaker/v2 v2.0.0 github.com/sony/sonyflake v1.2.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 diff --git a/go.sum b/go.sum index 015fea1b80..ad9e8914cf 100644 --- a/go.sum +++ b/go.sum @@ -670,6 +670,8 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sony/gobreaker/v2 v2.0.0 h1:23AaR4JQ65y4rz8JWMzgXw2gKOykZ/qfqYunll4OwJ4= +github.com/sony/gobreaker/v2 v2.0.0/go.mod h1:8JnRUz80DJ1/ne8M8v7nmTs2713i58nIt4s7XcGe/DI= github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ= github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= diff --git a/internal/cache/connector/redis/circuit_breaker.go b/internal/cache/connector/redis/circuit_breaker.go new file mode 100644 index 0000000000..1e06b7387e --- /dev/null +++ b/internal/cache/connector/redis/circuit_breaker.go @@ -0,0 +1,90 @@ +package redis + +import ( + "context" + "errors" + "time" + + "github.com/redis/go-redis/v9" + "github.com/sony/gobreaker/v2" + "github.com/zitadel/logging" +) + +const defaultInflightSize = 100000 + +type CBConfig struct { + // Interval when the counters are reset to 0. + // 0 interval never resets the counters until the CB is opened. + Interval time.Duration + // Amount of consecutive failures permitted + MaxConsecutiveFailures uint32 + // The ratio of failed requests out of total requests + MaxFailureRatio float64 + // Timeout after opening of the CB, until the state is set to half-open. + Timeout time.Duration + // The allowed amount of requests that are allowed to pass when the CB is half-open. + MaxRetryRequests uint32 +} + +func (config *CBConfig) readyToTrip(counts gobreaker.Counts) bool { + if config.MaxConsecutiveFailures > 0 && counts.ConsecutiveFailures > config.MaxConsecutiveFailures { + return true + } + if config.MaxFailureRatio > 0 && counts.Requests > 0 { + failureRatio := float64(counts.TotalFailures) / float64(counts.Requests) + return failureRatio > config.MaxFailureRatio + } + return false +} + +// limiter implements [redis.Limiter] as a circuit breaker. +type limiter struct { + inflight chan func(success bool) + cb *gobreaker.TwoStepCircuitBreaker[struct{}] +} + +func newLimiter(config *CBConfig, maxActiveConns int) redis.Limiter { + if config == nil { + return nil + } + // The size of the inflight channel needs to be big enough for maxActiveConns to prevent blocking. + // When that is 0 (no limit), we must set a sane default. + if maxActiveConns <= 0 { + maxActiveConns = defaultInflightSize + } + return &limiter{ + inflight: make(chan func(success bool), maxActiveConns), + cb: gobreaker.NewTwoStepCircuitBreaker[struct{}](gobreaker.Settings{ + Name: "redis cache", + MaxRequests: config.MaxRetryRequests, + Interval: config.Interval, + Timeout: config.Timeout, + ReadyToTrip: config.readyToTrip, + OnStateChange: func(name string, from, to gobreaker.State) { + logging.WithFields("name", name, "from", from, "to", to).Warn("circuit breaker state change") + }, + }), + } +} + +// Allow implements [redis.Limiter]. +func (l *limiter) Allow() error { + done, err := l.cb.Allow() + if err != nil { + return err + } + l.inflight <- done + return nil +} + +// ReportResult implements [redis.Limiter]. +// +// ReportResult checks the error returned by the Redis client. +// `nil`, [redis.Nil] and [context.Canceled] are not considered failures. +// Any other error, like connection or [context.DeadlineExceeded] is counted as a failure. +func (l *limiter) ReportResult(err error) { + done := <-l.inflight + done(err == nil || + errors.Is(err, redis.Nil) || + errors.Is(err, context.Canceled)) +} diff --git a/internal/cache/connector/redis/circuit_breaker_test.go b/internal/cache/connector/redis/circuit_breaker_test.go new file mode 100644 index 0000000000..ba61d18071 --- /dev/null +++ b/internal/cache/connector/redis/circuit_breaker_test.go @@ -0,0 +1,168 @@ +package redis + +import ( + "context" + "testing" + "time" + + "github.com/sony/gobreaker/v2" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/cache" +) + +func TestCBConfig_readyToTrip(t *testing.T) { + type fields struct { + MaxConsecutiveFailures uint32 + MaxFailureRatio float64 + } + type args struct { + counts gobreaker.Counts + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "disabled", + fields: fields{}, + args: args{ + counts: gobreaker.Counts{ + Requests: 100, + ConsecutiveFailures: 5, + TotalFailures: 10, + }, + }, + want: false, + }, + { + name: "no failures", + fields: fields{ + MaxConsecutiveFailures: 5, + MaxFailureRatio: 0.1, + }, + args: args{ + counts: gobreaker.Counts{ + Requests: 100, + ConsecutiveFailures: 0, + TotalFailures: 0, + }, + }, + want: false, + }, + { + name: "some failures", + fields: fields{ + MaxConsecutiveFailures: 5, + MaxFailureRatio: 0.1, + }, + args: args{ + counts: gobreaker.Counts{ + Requests: 100, + ConsecutiveFailures: 5, + TotalFailures: 10, + }, + }, + want: false, + }, + { + name: "consecutive exceeded", + fields: fields{ + MaxConsecutiveFailures: 5, + MaxFailureRatio: 0.1, + }, + args: args{ + counts: gobreaker.Counts{ + Requests: 100, + ConsecutiveFailures: 6, + TotalFailures: 0, + }, + }, + want: true, + }, + { + name: "ratio exceeded", + fields: fields{ + MaxConsecutiveFailures: 5, + MaxFailureRatio: 0.1, + }, + args: args{ + counts: gobreaker.Counts{ + Requests: 100, + ConsecutiveFailures: 1, + TotalFailures: 11, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := &CBConfig{ + MaxConsecutiveFailures: tt.fields.MaxConsecutiveFailures, + MaxFailureRatio: tt.fields.MaxFailureRatio, + } + if got := config.readyToTrip(tt.args.counts); got != tt.want { + t.Errorf("CBConfig.readyToTrip() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_redisCache_limiter(t *testing.T) { + c, _ := prepareCache(t, cache.Config{}, withCircuitBreakerOption( + &CBConfig{ + MaxConsecutiveFailures: 2, + MaxFailureRatio: 0.4, + Timeout: 100 * time.Millisecond, + MaxRetryRequests: 1, + }, + )) + + ctx := context.Background() + canceledCtx, cancel := context.WithCancel(ctx) + cancel() + timedOutCtx, cancel := context.WithTimeout(ctx, -1) + defer cancel() + + // CB is and should remain closed + for i := 0; i < 10; i++ { + err := c.Truncate(ctx) + require.NoError(t, err) + } + for i := 0; i < 10; i++ { + err := c.Truncate(canceledCtx) + require.ErrorIs(t, err, context.Canceled) + } + + // Timeout err should open the CB after more than 2 failures + for i := 0; i < 3; i++ { + err := c.Truncate(timedOutCtx) + if i > 2 { + require.ErrorIs(t, err, gobreaker.ErrOpenState) + } else { + require.ErrorIs(t, err, context.DeadlineExceeded) + } + } + + time.Sleep(200 * time.Millisecond) + + // CB should be half-open. If the first command fails, the CB will be Open again + err := c.Truncate(timedOutCtx) + require.ErrorIs(t, err, context.DeadlineExceeded) + err = c.Truncate(timedOutCtx) + require.ErrorIs(t, err, gobreaker.ErrOpenState) + + // Reset the DB to closed + time.Sleep(200 * time.Millisecond) + err = c.Truncate(ctx) + require.NoError(t, err) + + // Exceed the ratio + err = c.Truncate(timedOutCtx) + require.ErrorIs(t, err, context.DeadlineExceeded) + err = c.Truncate(ctx) + require.ErrorIs(t, err, gobreaker.ErrOpenState) +} diff --git a/internal/cache/connector/redis/connector.go b/internal/cache/connector/redis/connector.go index 2d0498dfa0..a10a0c25d0 100644 --- a/internal/cache/connector/redis/connector.go +++ b/internal/cache/connector/redis/connector.go @@ -105,6 +105,8 @@ type Config struct { // Add suffix to client name. Default is empty. IdentitySuffix string + + CircuitBreaker *CBConfig } type Connector struct { @@ -146,6 +148,7 @@ func optionsFromConfig(c Config) *redis.Options { ConnMaxLifetime: c.ConnMaxLifetime, DisableIndentity: c.DisableIndentity, IdentitySuffix: c.IdentitySuffix, + Limiter: newLimiter(c.CircuitBreaker, c.MaxActiveConns), } if c.EnableTLS { opts.TLSConfig = new(tls.Config) diff --git a/internal/cache/connector/redis/redis_test.go b/internal/cache/connector/redis/redis_test.go index 3f45be1502..1909f55e44 100644 --- a/internal/cache/connector/redis/redis_test.go +++ b/internal/cache/connector/redis/redis_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/alicebob/miniredis/v2" - "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/logging" @@ -689,26 +688,34 @@ func Test_redisCache_Truncate(t *testing.T) { } } -func prepareCache(t *testing.T, conf cache.Config) (cache.Cache[testIndex, string, *testObject], *miniredis.Miniredis) { +func prepareCache(t *testing.T, conf cache.Config, options ...func(*Config)) (cache.Cache[testIndex, string, *testObject], *miniredis.Miniredis) { conf.Log = &logging.Config{ Level: "debug", AddSource: true, } server := miniredis.RunT(t) server.Select(testDB) - client := redis.NewClient(&redis.Options{ - Network: "tcp", - Addr: server.Addr(), - }) + + connConfig := Config{ + Enabled: true, + Network: "tcp", + Addr: server.Addr(), + DisableIndentity: true, + } + for _, option := range options { + option(&connConfig) + } + connector := NewConnector(connConfig) t.Cleanup(func() { - client.Close() + connector.Close() server.Close() }) - connector := NewConnector(Config{ - Enabled: true, - Network: "tcp", - Addr: server.Addr(), - }) c := NewCache[testIndex, string, *testObject](conf, connector, testDB, testIndices) return c, server } + +func withCircuitBreakerOption(cb *CBConfig) func(*Config) { + return func(c *Config) { + c.CircuitBreaker = cb + } +} From b77901cb4b08e1e620228f2fb8e1a74fd8d3222c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 13 Nov 2024 23:18:47 +0200 Subject: [PATCH 07/32] fix(cache): unset client and user names in defaults (#8901) # Which Problems Are Solved By having default entries in the `Username` and `ClientName` fields, it was not possible to unset there parameters. Unsetting them is required for GCP connections # How the Problems Are Solved Set the fields to empty strings. # Additional Changes - none # Additional Context - none --- cmd/defaults.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index a188d1446b..4854356455 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -211,11 +211,11 @@ Caches: # host:port address. Addr: localhost:6379 # ClientName will execute the `CLIENT SETNAME ClientName` command for each conn. - ClientName: ZITADEL_cache + ClientName: "" # Use the specified Username to authenticate the current connection # with one of the connections defined in the ACL list when connecting # to a Redis 6.0 instance, or greater, that is using the Redis ACL system. - Username: zitadel + Username: "" # Optional password. Must match the password specified in the # requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower), # or the User Password when connecting to a Redis 6.0 instance, or greater, From 85bdf015054185cc411e2f6ee31faa7792d64d1a Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 14 Nov 2024 15:04:39 +0100 Subject: [PATCH 08/32] fix(actions): preserve order of execution (#8895) # Which Problems Are Solved The order of actions on a trigger was not respected on the execution and not correctly returned when retrieving the flow, for example in Console. The supposed correction of the order (e.g. in the UI) would then return a "no changes" error since the order was already as desired. # How the Problems Are Solved - Correctly order the actions of a trigger based on their configuration (`trigger_sequence`). # Additional Changes - replaced a `reflect.DeepEqual` with `slices.Equal` for checking the action list # Additional Context - reported by a customer - requires backports --- internal/command/org_flow.go | 4 ++-- internal/query/action_flow.go | 2 ++ internal/query/action_flow_test.go | 10 ++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/command/org_flow.go b/internal/command/org_flow.go index eff7a90286..24143aa96b 100644 --- a/internal/command/org_flow.go +++ b/internal/command/org_flow.go @@ -2,7 +2,7 @@ package command import ( "context" - "reflect" + "slices" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/repository/org" @@ -47,7 +47,7 @@ func (c *Commands) SetTriggerActions(ctx context.Context, flowType domain.FlowTy if err != nil { return nil, err } - if reflect.DeepEqual(existingFlow.Triggers[triggerType], actionIDs) { + if slices.Equal(existingFlow.Triggers[triggerType], actionIDs) { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nfh52", "Errors.Flow.NoChanges") } if len(actionIDs) > 0 { diff --git a/internal/query/action_flow.go b/internal/query/action_flow.go index d6d4243d94..c5263d6c43 100644 --- a/internal/query/action_flow.go +++ b/internal/query/action_flow.go @@ -168,6 +168,7 @@ func prepareTriggerActionsQuery(ctx context.Context, db prepareDatabase) (sq.Sel ). From(flowsTriggersTable.name). LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID) + db.Timetravel(call.Took(ctx))). + OrderBy(FlowsTriggersColumnTriggerSequence.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) ([]*Action, error) { actions := make([]*Action, 0) @@ -220,6 +221,7 @@ func prepareFlowQuery(ctx context.Context, db prepareDatabase, flowType domain.F ). From(flowsTriggersTable.name). LeftJoin(join(ActionColumnID, FlowsTriggersColumnActionID) + db.Timetravel(call.Took(ctx))). + OrderBy(FlowsTriggersColumnTriggerSequence.identifier()). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Flow, error) { flow := &Flow{ diff --git a/internal/query/action_flow_test.go b/internal/query/action_flow_test.go index 75f1ed22b4..af0db27278 100644 --- a/internal/query/action_flow_test.go +++ b/internal/query/action_flow_test.go @@ -33,8 +33,9 @@ var ( ` projections.flow_triggers3.sequence,` + ` projections.flow_triggers3.resource_owner` + ` FROM projections.flow_triggers3` + - ` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` - // ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` + + ` AS OF SYSTEM TIME '-1 ms'` + + ` ORDER BY projections.flow_triggers3.trigger_sequence` prepareFlowCols = []string{ "id", "creation_date", @@ -66,8 +67,9 @@ var ( ` projections.actions3.allowed_to_fail,` + ` projections.actions3.timeout` + ` FROM projections.flow_triggers3` + - ` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` - // ` AS OF SYSTEM TIME '-1 ms'` + ` LEFT JOIN projections.actions3 ON projections.flow_triggers3.action_id = projections.actions3.id AND projections.flow_triggers3.instance_id = projections.actions3.instance_id` + + ` AS OF SYSTEM TIME '-1 ms'` + + ` ORDER BY projections.flow_triggers3.trigger_sequence` prepareTriggerActionCols = []string{ "id", From 374b9a7f66046253da14dcc936fe8a3842385352 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 15 Nov 2024 07:19:43 +0100 Subject: [PATCH 09/32] fix(saml): provide option to get internal as default ACS (#8888) # Which Problems Are Solved Some SAML IdPs including Google only allow to configure a single AssertionConsumerService URL. Since the current metadata provides multiple and the hosted login UI is not published as neither the first nor with `isDefault=true`, those IdPs take another and then return an error on sign in. # How the Problems Are Solved Allow to reorder the ACS URLs using a query parameter (`internalUI=true`) when retrieving the metadata endpoint. This will list the `ui/login/login/externalidp/saml/acs` first and also set the `isDefault=true`. # Additional Changes None # Additional Context Reported by a customer --- internal/api/idp/idp.go | 62 ++++++--- internal/api/idp/integration_test/idp_test.go | 122 +++++++++++++++++- 2 files changed, 162 insertions(+), 22 deletions(-) diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index 3d46f029da..01594c43ba 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -8,9 +8,11 @@ import ( "fmt" "io" "net/http" + "strconv" "github.com/crewjam/saml" "github.com/gorilla/mux" + "github.com/muhlemmer/gu" "github.com/zitadel/logging" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -49,6 +51,7 @@ const ( paramError = "error" paramErrorDescription = "error_description" varIDPID = "idpid" + paramInternalUI = "internalUI" ) type Handler struct { @@ -187,21 +190,8 @@ func (h *Handler) handleMetadata(w http.ResponseWriter, r *http.Request) { } metadata := sp.ServiceProvider.Metadata() - for i, spDesc := range metadata.SPSSODescriptors { - spDesc.AssertionConsumerServices = append( - spDesc.AssertionConsumerServices, - saml.IndexedEndpoint{ - Binding: saml.HTTPPostBinding, - Location: h.loginSAMLRootURL(ctx), - Index: len(spDesc.AssertionConsumerServices) + 1, - }, saml.IndexedEndpoint{ - Binding: saml.HTTPArtifactBinding, - Location: h.loginSAMLRootURL(ctx), - Index: len(spDesc.AssertionConsumerServices) + 2, - }, - ) - metadata.SPSSODescriptors[i] = spDesc - } + internalUI, _ := strconv.ParseBool(r.URL.Query().Get(paramInternalUI)) + h.assertionConsumerServices(ctx, metadata, internalUI) buf, _ := xml.MarshalIndent(metadata, "", " ") w.Header().Set("Content-Type", "application/samlmetadata+xml") @@ -212,6 +202,48 @@ func (h *Handler) handleMetadata(w http.ResponseWriter, r *http.Request) { } } +func (h *Handler) assertionConsumerServices(ctx context.Context, metadata *saml.EntityDescriptor, internalUI bool) { + if !internalUI { + for i, spDesc := range metadata.SPSSODescriptors { + spDesc.AssertionConsumerServices = append( + spDesc.AssertionConsumerServices, + saml.IndexedEndpoint{ + Binding: saml.HTTPPostBinding, + Location: h.loginSAMLRootURL(ctx), + Index: len(spDesc.AssertionConsumerServices) + 1, + }, saml.IndexedEndpoint{ + Binding: saml.HTTPArtifactBinding, + Location: h.loginSAMLRootURL(ctx), + Index: len(spDesc.AssertionConsumerServices) + 2, + }, + ) + metadata.SPSSODescriptors[i] = spDesc + } + return + } + for i, spDesc := range metadata.SPSSODescriptors { + acs := make([]saml.IndexedEndpoint, 0, len(spDesc.AssertionConsumerServices)+2) + acs = append(acs, + saml.IndexedEndpoint{ + Binding: saml.HTTPPostBinding, + Location: h.loginSAMLRootURL(ctx), + Index: 0, + IsDefault: gu.Ptr(true), + }, + saml.IndexedEndpoint{ + Binding: saml.HTTPArtifactBinding, + Location: h.loginSAMLRootURL(ctx), + Index: 1, + }) + for i := 0; i < len(spDesc.AssertionConsumerServices); i++ { + spDesc.AssertionConsumerServices[i].Index = 2 + i + acs = append(acs, spDesc.AssertionConsumerServices[i]) + } + spDesc.AssertionConsumerServices = acs + metadata.SPSSODescriptors[i] = spDesc + } +} + func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) { ctx := r.Context() data := parseSAMLRequest(r) diff --git a/internal/api/idp/integration_test/idp_test.go b/internal/api/idp/integration_test/idp_test.go index 8e7141271a..d7616f8f53 100644 --- a/internal/api/idp/integration_test/idp_test.go +++ b/internal/api/idp/integration_test/idp_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" saml_xml "github.com/zitadel/saml/pkg/provider/xml" + "github.com/zitadel/saml/pkg/provider/xml/md" "golang.org/x/crypto/bcrypt" http_util "github.com/zitadel/zitadel/internal/api/http" @@ -111,13 +112,15 @@ func TestServer_SAMLMetadata(t *testing.T) { oauthIdpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) type args struct { - ctx context.Context - idpID string + ctx context.Context + idpID string + internalUI bool } tests := []struct { - name string - args args - want int + name string + args args + want int + wantACS []md.IndexedEndpointType }{ { name: "saml metadata, invalid idp", @@ -142,11 +145,115 @@ func TestServer_SAMLMetadata(t *testing.T) { idpID: samlRedirectIdpID, }, want: http.StatusOK, + wantACS: []md.IndexedEndpointType{ + { + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:metadata", + Local: "AssertionConsumerService", + }, + Index: "1", + IsDefault: "", + Binding: saml.HTTPPostBinding, + Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + samlRedirectIdpID + "/saml/acs", + ResponseLocation: "", + }, + { + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:metadata", + Local: "AssertionConsumerService", + }, + Index: "2", + IsDefault: "", + Binding: saml.HTTPArtifactBinding, + Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + samlRedirectIdpID + "/saml/acs", + ResponseLocation: "", + }, + { + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:metadata", + Local: "AssertionConsumerService", + }, + Index: "3", + IsDefault: "", + Binding: saml.HTTPPostBinding, + Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/ui/login/login/externalidp/saml/acs", + ResponseLocation: "", + }, + { + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:metadata", + Local: "AssertionConsumerService", + }, + Index: "4", + IsDefault: "", + Binding: saml.HTTPArtifactBinding, + Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/ui/login/login/externalidp/saml/acs", + ResponseLocation: "", + }, + }, + }, + { + name: "saml metadata, ok (internalUI)", + args: args{ + ctx: CTX, + idpID: samlRedirectIdpID, + internalUI: true, + }, + want: http.StatusOK, + wantACS: []md.IndexedEndpointType{ + { + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:metadata", + Local: "AssertionConsumerService", + }, + Index: "0", + IsDefault: "true", + Binding: saml.HTTPPostBinding, + Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/ui/login/login/externalidp/saml/acs", + ResponseLocation: "", + }, + { + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:metadata", + Local: "AssertionConsumerService", + }, + Index: "1", + IsDefault: "", + Binding: saml.HTTPArtifactBinding, + Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/ui/login/login/externalidp/saml/acs", + ResponseLocation: "", + }, + { + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:metadata", + Local: "AssertionConsumerService", + }, + Index: "2", + IsDefault: "", + Binding: saml.HTTPPostBinding, + Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + samlRedirectIdpID + "/saml/acs", + ResponseLocation: "", + }, + { + XMLName: xml.Name{ + Space: "urn:oasis:names:tc:SAML:2.0:metadata", + Local: "AssertionConsumerService", + }, + Index: "3", + IsDefault: "", + Binding: saml.HTTPArtifactBinding, + Location: http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + samlRedirectIdpID + "/saml/acs", + ResponseLocation: "", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { metadataURL := http_util.BuildOrigin(Instance.Host(), Instance.Config.Secure) + "/idps/" + tt.args.idpID + "/saml/metadata" + if tt.args.internalUI { + metadataURL = metadataURL + "?internalUI=true" + } resp, err := http.Get(metadataURL) assert.NoError(t, err) assert.Equal(t, tt.want, resp.StatusCode) @@ -155,10 +262,11 @@ func TestServer_SAMLMetadata(t *testing.T) { defer resp.Body.Close() assert.NoError(t, err) - _, err = saml_xml.ParseMetadataXmlIntoStruct(b) + metadata, err := saml_xml.ParseMetadataXmlIntoStruct(b) assert.NoError(t, err) - } + assert.Equal(t, metadata.SPSSODescriptor.AssertionConsumerService, tt.wantACS) + } }) } } From 7ba797b8724b5dfa7201e9e03097aa10a81a3511 Mon Sep 17 00:00:00 2001 From: Zach Hirschtritt Date: Fri, 15 Nov 2024 01:46:33 -0500 Subject: [PATCH 10/32] fix: use correct check for user existing on import (#8907) # Which Problems Are Solved - ImportHuman was not checking for a `UserStateDeleted` state on import, resulting in "already existing" errors when attempting to delete and re-import a user with the same id # How the Problems Are Solved Use the `Exists` helper method to check for both `UserStateUnspecified` and `UserStateDeleted` states on import # Additional Changes N/A # Additional Context N/A Co-authored-by: Livio Spring --- internal/command/user_human.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 91739e0d6d..ab2617c276 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -448,7 +448,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. return nil, nil, err } - if existing.UserState != domain.UserStateUnspecified { + if existing.UserState.Exists() { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-ziuna", "Errors.User.AlreadyExisting") } } From 45cf38e08f15d2dd445fd45050b7b6677f9dea7f Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 15 Nov 2024 16:48:11 +0100 Subject: [PATCH 11/32] chore: adding an adopters file for our community (#8909) # Which Problems Are Solved We want to give adopters a platform to show that they are using ZITADEL # How the Problems Are Solved Addding an ADOPTERS.md file # Additional Changes none # Additional Context none --- ADOPTERS.md | 9 +++++++++ README.md | 4 ++++ 2 files changed, 13 insertions(+) create mode 100644 ADOPTERS.md diff --git a/ADOPTERS.md b/ADOPTERS.md new file mode 100644 index 0000000000..3bb7e1a707 --- /dev/null +++ b/ADOPTERS.md @@ -0,0 +1,9 @@ +## Adopters + +We are grateful to the organizations and individuals who are using ZITADEL. If you are using ZITADEL, please consider adding your name to this list by submitting a pull request. + +| Organization/Individual | Contact Information | Description of Usage | +| ----------------------- | -------------------------------------------------------- | ----------------------------------------------- | +| ZITADEL | [@fforootd](https://github.com/fforootd) (and many more) | ZITADEL Cloud makes heavy use of of ZITADEL ;-) | +| Organization Name | contact@example.com | Description of how they use ZITADEL | +| Individual Name | contact@example.com | Description of how they use ZITADEL | diff --git a/README.md b/README.md index 27c66dbf81..17306129c4 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ Available data regions are: ZITADEL Cloud comes with a free tier, providing you with all the same features as the open-source version. Learn more about the [pay-as-you-go pricing](https://zitadel.com/pricing). +## Adopters + +We are grateful to the organizations and individuals who are using ZITADEL. If you are using ZITADEL, please consider adding your name to our [Adopters list](./ADOPTERS.md) by submitting a pull request. + ### Example applications Clone one of our [example applications](https://zitadel.com/docs/sdk-examples/introduction) or deploy them directly to Vercel. From fbebe0f183389489019c2e699d75be2b0dfb4407 Mon Sep 17 00:00:00 2001 From: Silvan Date: Fri, 15 Nov 2024 22:44:22 +0100 Subject: [PATCH 12/32] docs: init benchmarks (#8894) # Which Problems Are Solved Adds initial benchmarks. # How the Problems Are Solved Added section `apis/benchmarks` # Additional Changes Update Makefile dependencies # Additional Context - Part of https://github.com/zitadel/zitadel/issues/8023 - Part of https://github.com/zitadel/zitadel/issues/8352 --- docs/docs/apis/benchmarks/_template.mdx | 77 + docs/docs/apis/benchmarks/index.mdx | 111 + .../machine_jwt_profile_grant/index.mdx | 75 + .../machine_jwt_profile_grant/output.json | 1803 +++++++++++++++++ docs/package.json | 1 + docs/sidebars.js | 24 + docs/src/components/benchmark_chart.jsx | 45 + docs/static/img/benchmark/Flowchart.svg | 1 + docs/yarn.lock | 5 + 9 files changed, 2142 insertions(+) create mode 100644 docs/docs/apis/benchmarks/_template.mdx create mode 100644 docs/docs/apis/benchmarks/index.mdx create mode 100644 docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx create mode 100644 docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/output.json create mode 100644 docs/src/components/benchmark_chart.jsx create mode 100644 docs/static/img/benchmark/Flowchart.svg diff --git a/docs/docs/apis/benchmarks/_template.mdx b/docs/docs/apis/benchmarks/_template.mdx new file mode 100644 index 0000000000..a6a9109309 --- /dev/null +++ b/docs/docs/apis/benchmarks/_template.mdx @@ -0,0 +1,77 @@ + + +## Summary + +TODO: describe the outcome of the test? + +## Performance test results + +| Metric | Value | +|-:-------------------------------------|-:-----| +| Baseline | none | +| Purpose | | +| Test start | UTC | +| Test duration | 30min | +| Executed test | | +| k6 version | | +| VUs | | +| Client location | | +| Client machine specification | vCPU:
memory: Gb | +| ZITADEL location | | +| ZITADEL container specification | vCPU:
Memory: Gb
Container count: | +| ZITADEL Version | | +| ZITADEL Configuration | | +| ZITADEL feature flags | | +| Database | type: crdb / psql
version: | +| Database location | | +| Database specification | vCPU:
memory: Gb | +| ZITADEL metrics during test | | +| Observed errors | | +| Top 3 most expensive database queries | | +| Database metrics during test | | +| k6 Iterations per second | | +| k6 overview | | +| k6 output | | +| flowchart outcome | | + + +## Endpoint latencies + +import OutputSource from "!!raw-loader!./output.json"; + +import { BenchmarkChart } from '/src/components/benchmark_chart'; + + + +## k6 output {#k6-output} + +```bash +TODO: add summary of k6 +``` + diff --git a/docs/docs/apis/benchmarks/index.mdx b/docs/docs/apis/benchmarks/index.mdx new file mode 100644 index 0000000000..e5d89dbae8 --- /dev/null +++ b/docs/docs/apis/benchmarks/index.mdx @@ -0,0 +1,111 @@ +--- +title: Benchmarks +sidebar_label: Benchmarks +--- + +import DocCardList from '@theme/DocCardList'; + +Benchmarks are crucial to understand if ZITADEL fulfills your expected workload and what resources it needs to do so. + +This document explains the process and goals of load-testing zitadel in a cloud environment. + +The results can be found on sub pages. + +## Goals + +The primary goal is to assess if ZITADEL can scale to required proportion. The goals might change over time and maturity of ZITADEL. At the moment the goal is to assess how the application’s performance scales. There are some concrete goals we have to meet: + +1. [https://github.com/zitadel/zitadel/issues/8352](https://github.com/zitadel/zitadel/issues/8352) defines 1000 JWT profile auth/sec +2. [https://github.com/zitadel/zitadel/issues/4424](https://github.com/zitadel/zitadel/issues/4424) defines 1200 logins / sec. + +## Procedure + +First we determine the “target” of our load-test. The target is expressed as a make recipe in the load-test [Makefile](https://github.com/zitadel/zitadel/blob/main/load-test/Makefile). See also the load-test [readme](https://github.com/zitadel/zitadel/blob/main/load-test/README.md) on how to configure and run load-tests. +A target should be tested for longer periods of time, as it might take time for certain metrics to show up. For example, cloud SQL samples query insights. A runtime of at least **30 minutes** is advised at the moment. + +After each iteration of load-test, we should consult the [After test procedure](#after-test-procedure) to conclude an outcome: + +1. Scale +2. Log potential issuer and scale +3. Terminate testing and resolve issues + + +## Methodology + +### Benchmark definition + +Tests are implemented in the ecosystem of [k6](https://k6.io). The tests are publicly available in the [zitadel repository](https://github.com/zitadel/zitadel/tree/main/load-test). Custom extensions of k6 are implemented in the [xk6-modules repository](https://github.com/zitadel/xk6-modules). +The tests must at least measure the request duration for each API call. This gives an indication on how zitadel behaves over the duration of the load test. + +### Metrics + +The following metrics must be collected for each test iteration. The metrics are used to follow the decision path of the [After test procedure](https://drive.google.com/open?id=1WVr7aA8dGgV1zd2jUg1y1h_o37mkZF2O6M5Mhafn_NM): + +| Metric | Type | Description | Unit | +| :---- | :---- | :---- | :---- | +| Baseline | Comparison | Defines the baseline the test is compared against. If not specified the baseline defined in this document is used. | Link to test result | +| Purpose | Description | Description what should been proved with this test run | text +| Test start | Setup | Timestamp when the test started. This is useful for gathering additional data like metrics or logs later | Date | +| Test duration | Setup | Duration of the test | Duration | +| Executed test | Setup | Name of the make recipe executed. Further information about specific test cases can be found [here](?tab=t.0#heading=h.xav4f3s5r2f3). | Name of the make recipe | +| k6 version | Setup | Version of the test client (k6) used | semantic version | +| VUs | Setup | Virtual Users which execute the test scenario in parallel | Number | +| Client location | Setup | Region or location of the machine which executed the test client. If not further specified the hoster is Google Cloud | Location / Region | +| Client machine specification | Setup | Definition of the client machine the test client ran on. The resources of the machine could be maxed out during tests therefore we collect this metric as well. The description must at least clarify the following metrics: vCPU Memory egress bandwidth | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps | +| ZITADEL location | Setup | Region or location of the deployment of zitadel. If not further specified the hoster is Google Cloud | Location / Region | +| ZITADEL container specification | Setup | As ZITADEL is mainly run in cloud environments it should also be run as a container during the load tests. The description must at least clarify the following metrics: vCPU Memory egress bandwidth Scale | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps **scale**: The amount of containers running during the test. The amount must not vary during the tests | +| ZITADEL Version | Setup | The version of zitadel deployed | Semantic version or commit | +| ZITADEL Configuration | Setup | Configuration of zitadel which deviates from the defaults and is not secret | yaml | +| ZITADEL feature flags | Setup | Changed feature flags | yaml | +| Database | Setup | Database type and version | **type**: crdb / psql **version**: semantic version | +| Database location | Setup | Region or location of the deployment of the database. If not further specified the hoster is Google Cloud SQL | Location / Region | +| Database specification | Setup | The description must at least clarify the following metrics: vCPU, Memory and egress bandwidth (Scale) | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps **scale**: Amount of crdb nodes if crdb is used | +| ZITADEL metrics during test | Result | This metric helps understanding the bottlenecks of the executed test. At least the following metrics must be provided: CPU usage Memory usage | **CPU usage** in percent **Memory usage** in percent | +| Observed errors | Result | Errors worth mentioning, mostly unexpected errors | description | +| Top 3 most expensive database queries | Result | The execution plan of the top 3 most expensive database queries during the test execution | database execution plan | +| Database metrics during test | Result | This metric helps understanding the bottlenecks of the executed test. At least the following metrics must be provided: CPU usage Memory usage | **CPU usage** in percent **Memory usage** in percent | +| k6 Iterations per second | Result | How many test iterations were done per second | Number | +| k6 overview | Result | Shows some basic metrics aggregated over the test run At least the following metrics must be included: duration per request (min, max, avg, p50, p95, p99) VUS For simplicity just add the whole test result printed to the terminal | terminal output | +| k6 output | Result | Trends and metrics generated during the test, this contains detailed information for each step executed during each iteration | csv | + +### Test setup + +#### Make recipes + +Details about the tests implemented can be found in [this readme](https://github.com/zitadel/zitadel/blob/main/load-test/README.md#test). + +### Test conclusion + +After each iteration of load-test, we should consult the [Flowchart](#after-test-procedure) to conclude an outcome: + +1. [Scale](#scale) +2. [Log potential issue and scale](#potential-issues) +3. [Terminate testing](#termination) and resolve issues + +#### Scale {#scale} + +An outcome of scale means that the service hit some kind of resource limit, like CPU or RAM which can be increased. In such cases we increase the suggested parameter and rerun the load-test for the same target. On the next test we should analyse if the increase in scale resulted in a performance improvement proportional to the scale parameter. For example if we scale from 1 to 2 containers, it might be reasonable to expect a doubling of iterations / sec. If such an increase is not noticed, there might be another bottleneck or unlying issue, such as locking. + +#### Potential issues {#potential-issues} + +A potential issue has an impact on performance, but does not prevent us to scale. Such issues must be logged in GH issues and load-testing can continue. The issue can be resolved at a later time and the load-tests repeated when it is. This is primarily for issues which require big changes to ZITADEL. + +#### Termination {#termination} + +Scaling no longer improves iterations / second, or some kind of critical error or bug is experienced. The root cause of the issue must be resolved before we can continue with increasing scale. + +### After test procedure + +This flowchart shows the procedure after running a test. + +![Flowchart](/img/benchmark/Flowchart.svg) + +## Baseline + +Will be established as soon as the goal described above is reached. + +## Test results + +This chapter provides a table linking to the detailed test results. + + diff --git a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx new file mode 100644 index 0000000000..f2e198c510 --- /dev/null +++ b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx @@ -0,0 +1,75 @@ +--- +title: machine jwt profile grant benchmark of zitadel v2.65.0 +sidebar_label: machine jwt profile grant +--- + +## Summary + +Tests are halted after this test run because of too many [client read events](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/wait-event.clientread.html) on the database. + +## Performance test results + +| Metric | Value | +| :---- | :---- | +| Baseline | none | +| Test start | 22-10-2024 16:20 UTC | +| Test duration | 30min | +| Executed test | machine\_jwt\_profile\_grant | +| k6 version | v0.54.0 | +| VUs | 50 | +| Client location | US1 | +| Client machine specification | e2-high-cpu-4 | +| Zitadel location | US1 | +| Zitadel container specification | vCPUs: 2
Memory: 512 MiB
Container count: 2 | +| Zitadel feature flags | none | +| Database | postgres v15 | +| Database location | US1 | +| Database specification | vCPUs: 4
Memory: 16 GiB | +| Zitadel metrics during test | | +| Observed errors | Many client read events during push | +| Top 3 most expensive database queries | 1: Query events `instance_id = $1 AND aggregate_type = $2 AND aggregate_id = $3 AND event_type = ANY($4)`
2: latest sequence query during push events
3: writing events during push (caused lock wait events) | +| k6 iterations per second | 193 | +| k6 overview | [output](#k6-output) | +| flowchart outcome | Halt tests, must resolve an issue | + +## /token endpoint latencies + +import OutputSource from "!!raw-loader!./output.json"; + +import { BenchmarkChart } from '/src/components/benchmark_chart'; + + + +## k6 output {#k6-output} + +```bash +checks...............................: 100.00% ✓ 695739 ✗ 0 +data_received........................: 479 MB 265 kB/s +data_sent............................: 276 MB 153 kB/s +http_req_blocked.....................: min=178ns avg=5µs max=119.8ms p(50)=460ns p(95)=702ns p(99)=921ns +http_req_connecting..................: min=0s avg=1.24µs max=43.45ms p(50)=0s p(95)=0s p(99)=0s +http_req_duration....................: min=18ms avg=255.3ms max=1.22s p(50)=241.56ms p(95)=479.19ms p(99)=600.92ms +{ expected_response:true }.........: min=18ms avg=255.3ms max=1.22s p(50)=241.56ms p(95)=479.19ms p(99)=600.92ms +http_req_failed......................: 0.00% ✓ 0 ✗ 347998 +http_req_receiving...................: min=25.92µs avg=536.96µs max=401.94ms p(50)=89.44µs p(95)=2.39ms p(99)=11.12ms +http_req_sending.....................: min=24.01µs avg=63.86µs max=4.48ms p(50)=60.97µs p(95)=88.69µs p(99)=141.74µs +http_req_tls_handshaking.............: min=0s avg=2.8µs max=51.05ms p(50)=0s p(95)=0s p(99)=0s +http_req_waiting.....................: min=17.65ms avg=254.7ms max=1.22s p(50)=240.88ms p(95)=478.6ms p(99)=600.6ms +http_reqs............................: 347998 192.80552/s +iteration_duration...................: min=33.86ms avg=258.77ms max=1.22s p(50)=245ms p(95)=482.61ms p(99)=604.32ms +iterations...........................: 347788 192.689171/s +login_ui_enter_login_name_duration...: min=218.61ms avg=218.61ms max=218.61ms p(50)=218.61ms p(95)=218.61ms p(99)=218.61ms +login_ui_enter_password_duration.....: min=18ms avg=18ms max=18ms p(50)=18ms p(95)=18ms p(99)=18ms +login_ui_init_login_duration.........: min=90.96ms avg=90.96ms max=90.96ms p(50)=90.96ms p(95)=90.96ms p(99)=90.96ms +login_ui_token_duration..............: min=140.02ms avg=140.02ms max=140.02ms p(50)=140.02ms p(95)=140.02ms p(99)=140.02ms +oidc_token_duration..................: min=29.85ms avg=255.38ms max=1.22s p(50)=241.61ms p(95)=479.23ms p(99)=600.95ms +org_create_org_duration..............: min=64.51ms avg=64.51ms max=64.51ms p(50)=64.51ms p(95)=64.51ms p(99)=64.51ms +user_add_machine_key_duration........: min=44.93ms avg=87.89ms max=159.52ms p(50)=84.43ms p(95)=144.59ms p(99)=155.54ms +user_create_machine_duration.........: min=65.75ms avg=266.53ms max=421.58ms p(50)=276.59ms p(95)=380.84ms p(99)=414.43ms +vus..................................: 0 min=0 max=50 +vus_max..............................: 50 min=50 max=50 + +running (30m04.9s), 00/50 VUs, 347788 complete and 0 interrupted iterations +default ✓ [======================================] 50 VUs 30m0s +``` + diff --git a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/output.json b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/output.json new file mode 100644 index 0000000000..c2316eae29 --- /dev/null +++ b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/output.json @@ -0,0 +1,1803 @@ +[ + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:35+02","p50":152.742548,"p95":189.4438704479804,"p99":214.3874318037758}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:36+02","p50":134.31271792,"p95":176.85990823356335,"p99":189.17410793315742}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:37+02","p50":149.12034540000002,"p95":234.05779399949907,"p99":246.90676967010307}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:38+02","p50":141.79488025,"p95":298.87885997760134,"p99":314.37352053326987}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:39+02","p50":183.23340781250002,"p95":283.2726692992518,"p99":292.49417304653673}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:40+02","p50":213.86190366666668,"p95":284.20557151433445,"p99":390.4803307008381}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:41+02","p50":78.26700088888889,"p95":445.6252991799176,"p99":464.8756637391315}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:42+02","p50":93.90013371875,"p95":393.90886492717925,"p99":404.07198386581877}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:43+02","p50":99.26241149999998,"p95":456.46543169375514,"p99":482.1890742862892}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:44+02","p50":259.33973561111117,"p95":387.0035960871835,"p99":402.3678526092014}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:45+02","p50":163.7966285,"p95":280.9855007966218,"p99":297.6403551814313}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:46+02","p50":74.6244951875,"p95":394.0813611938515,"p99":412.85163931081155}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:47+02","p50":180.3818921111111,"p95":391.04031958131907,"p99":409.423791903923}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:48+02","p50":196.83148822222222,"p95":390.84742817174293,"p99":422.014462269928}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:49+02","p50":192.46919175,"p95":261.7432952727178,"p99":297.98025934050463}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:50+02","p50":163.3955145,"p95":373.8762231012586,"p99":402.81260525012493}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:51+02","p50":169.287491,"p95":252.19139975148394,"p99":260.0374195735345}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:52+02","p50":172.3803739375,"p95":254.90392808387185,"p99":298.5229499636707}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:53+02","p50":159.63037021875002,"p95":325.96280276943094,"p99":339.4366488568575}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:54+02","p50":151.7369948125,"p95":232.77033866258023,"p99":245.4879663243437}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:55+02","p50":146.12840375000002,"p95":276.01754821507706,"p99":286.9390759050336}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:56+02","p50":166.82458387499997,"p95":244.8316436244864,"p99":255.96774487884497}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:57+02","p50":171.2386103125,"p95":231.91869474922763,"p99":255.3471724571259}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:58+02","p50":132.43773015625,"p95":305.5849403249092,"p99":317.7783982236719}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:20:59+02","p50":98.29408981249999,"p95":345.2765417731901,"p99":359.39837518970154}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:00+02","p50":174.8437813125,"p95":279.72543384122116,"p99":295.9230939623306}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:01+02","p50":156.1206249375,"p95":257.0086383694033,"p99":290.1174500658493}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:02+02","p50":125.861364375,"p95":335.8766849864509,"p99":344.69110596878573}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:03+02","p50":116.19827,"p95":289.9331628960681,"p99":308.2670122635899}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:04+02","p50":88.807123,"p95":309.88822512970404,"p99":329.24833508430766}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:05+02","p50":125.42367399999999,"p95":313.7408337804247,"p99":330.610476359817}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:06+02","p50":148.7421011875,"p95":288.69967697102777,"p99":307.14991885160015}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:07+02","p50":123.452076,"p95":324.4732774677882,"p99":338.0723913875208}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:08+02","p50":94.517341,"p95":310.1070211770424,"p99":322.64246295973874}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:09+02","p50":199.0995741875,"p95":266.4028019143426,"p99":283.33593921839906}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:10+02","p50":159.412726375,"p95":231.16968194039453,"p99":236.0824995757258}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:11+02","p50":162.84928418750002,"p95":252.365319447649,"p99":276.632285485939}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:12+02","p50":145.54211509375,"p95":264.97463031152597,"p99":277.07558885515954}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:13+02","p50":137.4853945625,"p95":383.14112161791684,"p99":401.6204909024353}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:14+02","p50":110.63827400000001,"p95":324.8113530617447,"p99":348.8752330086517}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:15+02","p50":180.9934134375,"p95":294.84876206776056,"p99":304.4406698862913}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:16+02","p50":211.72708266666666,"p95":253.80525846297198,"p99":269.59085696040177}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:17+02","p50":209.98856577777778,"p95":253.06196131151407,"p99":264.4179178528454}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:18+02","p50":156.35097040625,"p95":293.5385406069375,"p99":401.6490299732654}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:19+02","p50":133.221681875,"p95":399.4656152824501,"p99":418.854191442893}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:20+02","p50":118.86549344444445,"p95":405.91692504971184,"p99":417.90231864442757}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:21+02","p50":191.00690390625,"p95":305.106348396054,"p99":334.6109268334396}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:22+02","p50":131.788673875,"p95":318.6432634163847,"p99":340.73336806457513}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:23+02","p50":119.2033654375,"p95":278.4747170609107,"p99":290.79545231027504}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:24+02","p50":103.891219625,"p95":354.52017043143763,"p99":365.99442424978156}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:25+02","p50":110.90601099999999,"p95":409.53373378248904,"p99":439.4092664350481}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:26+02","p50":110.04638775000001,"p95":353.0635395443648,"p99":390.9164269659906}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:27+02","p50":139.166329,"p95":330.2447540706727,"p99":343.60334007957596}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:28+02","p50":173.25523311111112,"p95":353.7907797741324,"p99":362.92556865444755}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:29+02","p50":268.14744616666667,"p95":350.00666472811616,"p99":364.6646985822191}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:30+02","p50":215.313930875,"p95":273.1942162963142,"p99":288.4021928619313}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:31+02","p50":151.94144256250001,"p95":302.8211043623749,"p99":316.1713350709291}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:32+02","p50":204.56433033333334,"p95":350.7035384562023,"p99":367.32154349208736}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:33+02","p50":184.935939625,"p95":244.05362925004005,"p99":265.5505863892994}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:34+02","p50":181.39206394444446,"p95":263.64057324885795,"p99":279.00201451316264}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:35+02","p50":205.52729209375002,"p95":266.2276509014458,"p99":275.20543244661326}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:36+02","p50":184.00616190625001,"p95":337.11831113507805,"p99":366.7406586168554}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:37+02","p50":175.3103945,"p95":395.91543717141724,"p99":416.1321127693481}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:38+02","p50":170.106749,"p95":233.25833817163755,"p99":255.1431567000685}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:39+02","p50":175.60114431250003,"p95":251.45913770202392,"p99":271.3119037283981}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:40+02","p50":177.2062990625,"p95":294.1022916899325,"p99":307.97861875463434}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:41+02","p50":177.1961231875,"p95":320.58236328688787,"p99":336.7532011867485}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:42+02","p50":162.4275845,"p95":361.9094602270508,"p99":385.3591560391846}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:43+02","p50":189.32301237500002,"p95":356.5451459530969,"p99":376.63635141138747}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:44+02","p50":145.7136858125,"p95":344.11261977926944,"p99":359.2462192147102}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:45+02","p50":172.38470781249998,"p95":353.6295055632823,"p99":372.84916331113646}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:46+02","p50":206.59438666666668,"p95":441.1500573161624,"p99":474.9812698063977}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:47+02","p50":172.873287375,"p95":320.8506584619575,"p99":358.5100670831585}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:48+02","p50":169.6534998125,"p95":303.4536858748769,"p99":330.31731258393904}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:49+02","p50":173.02729125000002,"p95":298.78449271407396,"p99":339.62662085937404}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:50+02","p50":163.46804384375002,"p95":296.1723000890368,"p99":315.01419666908794}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:51+02","p50":171.338443,"p95":248.84376248058607,"p99":259.60469639209316}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:52+02","p50":163.3185848125,"p95":270.9658209938422,"p99":290.1653841856108}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:53+02","p50":147.25774171875003,"p95":263.3940735966012,"p99":284.50598333436756}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:54+02","p50":176.44091971875,"p95":266.5990478841503,"p99":300.03993930488224}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:55+02","p50":142.82466225,"p95":289.0258727321505,"p99":300.2080874461651}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:56+02","p50":175.36170518749998,"p95":330.08911763715554,"p99":375.1193799273758}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:57+02","p50":162.222671625,"p95":281.79289915946663,"p99":294.4279530910194}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:58+02","p50":143.71196425,"p95":362.0538523995184,"p99":381.575093799727}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:21:59+02","p50":149.01801776000002,"p95":195.78059584427396,"p99":209.09926001296807}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:00+02","p50":168.380417625,"p95":267.1608095025909,"p99":282.49195416929626}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:01+02","p50":128.7791685,"p95":262.1669432662675,"p99":270.3051170505457}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:02+02","p50":127.9593894375,"p95":252.83218249204972,"p99":274.0372604026485}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:03+02","p50":127.1617695,"p95":267.06822044613585,"p99":283.4020022676983}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:04+02","p50":106.68187865625,"p95":307.734150013091,"p99":316.84643377242423}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:05+02","p50":179.382695,"p95":249.17453328077286,"p99":266.2734021554391}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:06+02","p50":172.73768656250002,"p95":228.5491930327513,"p99":264.98035601136826}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:07+02","p50":128.65640771875002,"p95":337.6269146065091,"p99":371.7743257245612}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:08+02","p50":100.0326530625,"p95":303.43871650302395,"p99":334.7614539394679}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:09+02","p50":119.12006821875,"p95":335.46215280524103,"p99":350.0083065727637}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:10+02","p50":125.806114,"p95":288.15592741759167,"p99":301.6352730747447}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:11+02","p50":133.42285409375,"p95":316.6540532220981,"p99":329.05199164620757}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:12+02","p50":95.71434059375,"p95":308.4402244789036,"p99":320.67318234980775}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:13+02","p50":155.3043468125,"p95":394.8728883021291,"p99":414.4798793780084}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:14+02","p50":164.24609787499998,"p95":295.26223958864114,"p99":312.8365331274672}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:15+02","p50":140.62968171875002,"p95":297.0755794801937,"p99":311.255825160466}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:16+02","p50":167.9410644375,"p95":338.7029358086513,"p99":354.43729884373784}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:17+02","p50":155.62452675,"p95":282.73366318783974,"p99":320.7858698425536}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:18+02","p50":162.254539375,"p95":324.6161681515989,"p99":340.7320104777489}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:19+02","p50":145.28746665625,"p95":309.1353151952988,"p99":321.34194833209347}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:20+02","p50":155.42283681249998,"p95":227.0559183476073,"p99":285.1077931350944}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:21+02","p50":147.71090025,"p95":330.71194199860406,"p99":343.6829263263979}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:22+02","p50":161.43343950000002,"p95":224.56334413866048,"p99":235.36415136311962}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:23+02","p50":179.38427725,"p95":249.76000027644014,"p99":260.70670063443185}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:24+02","p50":225.33285412500004,"p95":281.72253456857135,"p99":299.9099734186788}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:25+02","p50":166.78473862500002,"p95":232.1957648318806,"p99":299.35293979125305}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:26+02","p50":172.81051031249999,"p95":338.5984408025247,"p99":365.33059938493534}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:27+02","p50":180.41219675,"p95":254.84395884091364,"p99":280.9361037687831}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:28+02","p50":170.17445437499998,"p95":244.8935166378977,"p99":282.9037624571953}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:29+02","p50":196.90349796875,"p95":264.5450147089112,"p99":284.2996830777185}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:30+02","p50":149.61717309375,"p95":258.4944096041763,"p99":280.0767955062025}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:31+02","p50":115.37371856249999,"p95":342.0609913932303,"p99":369.31108017582085}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:32+02","p50":198.97898015625,"p95":275.73095247318406,"p99":330.507157946228}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:33+02","p50":147.3347025,"p95":324.7720196622293,"p99":338.60460008617355}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:34+02","p50":118.407053,"p95":281.2860384632016,"p99":326.8983674016011}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:35+02","p50":152.8172223125,"p95":271.18395431658706,"p99":314.1567188235145}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:36+02","p50":133.17060325,"p95":331.59060301692466,"p99":343.9984966597519}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:37+02","p50":139.62863121875,"p95":284.8950458756842,"p99":294.2825206762035}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:38+02","p50":180.022578625,"p95":258.0588529323397,"p99":289.3623059911065}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:39+02","p50":157.79121866666665,"p95":299.27330007239976,"p99":317.4350921771674}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:40+02","p50":208.92391650000002,"p95":344.031906960927,"p99":363.73983160268403}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:41+02","p50":202.68174344444444,"p95":305.52328825860576,"p99":337.10060789617324}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:42+02","p50":84.37220403124999,"p95":361.3133458600537,"p99":371.2432742504656}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:43+02","p50":231.49854371875,"p95":349.888320585701,"p99":384.09872475423884}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:44+02","p50":183.9890441875,"p95":271.10930583288854,"p99":298.76474763219386}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:45+02","p50":182.8464445,"p95":246.2624996970563,"p99":271.49484056651687}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:46+02","p50":93.68166966666666,"p95":463.97366856742735,"p99":481.7509814693787}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:47+02","p50":115.90802022222222,"p95":457.24317004265833,"p99":481.81383054922867}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:48+02","p50":127.41821911111111,"p95":444.18740387771396,"p99":455.7524692107775}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:49+02","p50":139.04014443749998,"p95":294.4670809434862,"p99":305.7424495887248}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:50+02","p50":98.5424918125,"p95":347.32745104648734,"p99":363.6402334598913}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:51+02","p50":119.86771771875,"p95":261.4503544709988,"p99":271.10387682557536}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:52+02","p50":130.5921068125,"p95":335.50228157148223,"p99":345.1897153513317}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:53+02","p50":147.82731846875,"p95":305.32917860643096,"p99":327.81091039268375}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:54+02","p50":184.37881875,"p95":414.9498528464594,"p99":436.0652476570034}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:55+02","p50":134.9334113888889,"p95":394.22851781011155,"p99":423.1439984321022}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:56+02","p50":94.6194291875,"p95":369.1436656720502,"p99":429.596956807251}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:57+02","p50":103.10241853124998,"p95":358.68138499470405,"p99":369.60892119685246}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:58+02","p50":96.860279125,"p95":298.3693310251786,"p99":321.95436359483097}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:22:59+02","p50":126.16320309375,"p95":358.2172364309369,"p99":380.3571782561917}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:00+02","p50":163.9286190625,"p95":287.8749754242624,"p99":304.07535824718906}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:01+02","p50":110.296562,"p95":356.67980462961077,"p99":369.1336770410352}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:02+02","p50":121.2899105625,"p95":376.36945194076355,"p99":393.2726901395225}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:03+02","p50":118.94529322222222,"p95":364.647289347859,"p99":379.42877625152425}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:04+02","p50":168.66469700000002,"p95":335.5127914306146,"p99":348.81079129140187}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:05+02","p50":109.39908218750001,"p95":343.64168488864436,"p99":356.2030261326771}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:06+02","p50":90.71489159375001,"p95":395.67789852777827,"p99":414.08672484933857}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:07+02","p50":102.102573,"p95":479.7651860054539,"p99":509.84110983474443}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:08+02","p50":86.0721990625,"p95":506.7270854694948,"p99":533.6720435621214}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:09+02","p50":156.2318160625,"p95":342.99648317675934,"p99":372.053304515549}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:10+02","p50":188.14600168750002,"p95":306.32164278457384,"p99":363.1293493959436}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:11+02","p50":192.2799678888889,"p95":300.28397298561293,"p99":314.459901629174}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:12+02","p50":153.60912234375002,"p95":411.5785920044621,"p99":428.19369271948545}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:13+02","p50":111.96040766666665,"p95":435.55265119979356,"p99":452.5854287691793}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:14+02","p50":139.76533733333335,"p95":455.35024341441203,"p99":475.24830211597254}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:15+02","p50":198.77428865625,"p95":325.2470845571967,"p99":354.2798636108484}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:16+02","p50":127.02826,"p95":397.94563417990844,"p99":418.64960824809697}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:17+02","p50":106.55520544444444,"p95":452.29131266254143,"p99":470.03969402793314}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:18+02","p50":113.88863299999998,"p95":525.0521919118939,"p99":529.8129106062248}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:19+02","p50":111.85406478125,"p95":389.74024184425025,"p99":402.0315347526336}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:20+02","p50":175.7656145,"p95":347.27385680249597,"p99":358.86244784611324}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:21+02","p50":157.78282825,"p95":317.69338495123293,"p99":341.9171689902344}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:22+02","p50":131.46160666666665,"p95":369.19526349180256,"p99":387.8415062052717}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:23+02","p50":204.50269084374997,"p95":354.0385939407066,"p99":375.9711954611883}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:24+02","p50":169.301663,"p95":321.4563997478036,"p99":339.69673819850203}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:25+02","p50":263.8962338333334,"p95":347.43064110933443,"p99":372.9916210012765}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:26+02","p50":239.87338566666668,"p95":347.0042002027512,"p99":389.0801205401683}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:27+02","p50":187.87346983333336,"p95":378.2293020718398,"p99":392.0665997422705}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:28+02","p50":278.3228516111111,"p95":413.9842850428133,"p99":420.58175994584656}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:29+02","p50":230.29268366666668,"p95":388.3813619979001,"p99":414.3347986147597}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:30+02","p50":205.597269,"p95":342.4151837143591,"p99":372.75280997233176}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:31+02","p50":185.66734133333333,"p95":366.79302571517263,"p99":386.82752764575815}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:32+02","p50":113.17833633333333,"p95":382.2272483782675,"p99":399.6901822997966}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:33+02","p50":224.427364,"p95":318.78927568640904,"p99":379.3479040931482}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:34+02","p50":171.93366183333333,"p95":337.67122743761195,"p99":347.5229087776327}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:35+02","p50":160.31750171875,"p95":296.4358263359214,"p99":305.3764371393015}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:36+02","p50":190.399403125,"p95":329.76192083518026,"p99":358.4830148763714}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:37+02","p50":189.664540625,"p95":291.7815892821846,"p99":306.7206485249286}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:38+02","p50":112.06204344444444,"p95":369.52459361610045,"p99":409.1660215970068}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:39+02","p50":276.3946736111111,"p95":403.4417701618221,"p99":483.7071277961846}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:40+02","p50":214.50907466666663,"p95":368.17170445707075,"p99":387.5157735438919}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:41+02","p50":91.36098566666665,"p95":401.8118318129068,"p99":417.86146135524916}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:42+02","p50":237.6363456875,"p95":333.55795506752133,"p99":345.2556038274617}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:43+02","p50":208.2464379375,"p95":285.59666313684176,"p99":306.4541200015583}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:44+02","p50":108.37741083333333,"p95":381.99226537324716,"p99":399.8603670226402}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:45+02","p50":224.89928093749998,"p95":356.48579109293877,"p99":370.34997563481807}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:46+02","p50":187.40087555555556,"p95":365.9536206192869,"p99":395.0626114183414}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:47+02","p50":219.2488168333333,"p95":352.2197022321227,"p99":360.55958806999826}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:48+02","p50":128.14031988888888,"p95":370.29248196576475,"p99":381.02069397623825}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:49+02","p50":175.8623413125,"p95":328.54612543744935,"p99":351.48333055523966}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:50+02","p50":170.20777471875,"p95":248.07310912902372,"p99":259.2663498275757}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:51+02","p50":189.34810149999998,"p95":254.63217625896917,"p99":271.4447753241811}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:52+02","p50":155.339734625,"p95":247.59586218575285,"p99":297.08098081845475}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:53+02","p50":162.452478,"p95":235.18443832070545,"p99":251.61101743764354}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:54+02","p50":197.10812044444447,"p95":340.2054231095977,"p99":372.61731303294516}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:55+02","p50":148.3748315625,"p95":304.8272034887056,"p99":328.65983394512176}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:56+02","p50":112.314109625,"p95":371.49208539474535,"p99":389.60865558733843}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:57+02","p50":84.32444275,"p95":344.43789664394535,"p99":355.5426731411834}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:58+02","p50":125.430037875,"p95":363.9490895496088,"p99":386.1311214982054}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:23:59+02","p50":113.5846795,"p95":339.92753503626835,"p99":357.93413792771963}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:00+02","p50":112.61793766666666,"p95":352.45506782568015,"p99":357.97922475105383}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:01+02","p50":161.57308468749997,"p95":330.8013208029374,"p99":348.19938232203555}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:02+02","p50":88.23626255555557,"p95":411.7930310908556,"p99":436.56527862182617}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:03+02","p50":155.04360022222224,"p95":363.3401143622393,"p99":378.03465003131817}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:04+02","p50":134.3302317777778,"p95":442.9010524626141,"p99":476.7326678590927}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:05+02","p50":132.83108988888887,"p95":407.68930056825303,"p99":427.3419876943593}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:06+02","p50":235.7166078333333,"p95":427.09760607859795,"p99":450.61696512708517}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:07+02","p50":185.73770733333333,"p95":345.20340229425943,"p99":369.64291598097344}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:08+02","p50":127.86131288888889,"p95":346.50813979244816,"p99":381.3701055143173}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:09+02","p50":204.58561616666668,"p95":385.83124888707266,"p99":404.65335307042693}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:10+02","p50":134.93217372222225,"p95":335.8876011389651,"p99":344.50895744095425}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:11+02","p50":145.890436125,"p95":371.3015613146542,"p99":380.6078602409513}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:12+02","p50":137.35259468749996,"p95":320.9258905171859,"p99":341.68218627207045}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:13+02","p50":109.34771977777778,"p95":501.9964330002911,"p99":515.4457118845553}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:14+02","p50":100.66297100000001,"p95":463.74451614195823,"p99":513.6433440599309}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:15+02","p50":109.57291790625,"p95":371.5828618090343,"p99":388.3091798479202}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:16+02","p50":150.32008,"p95":468.928458806547,"p99":488.85305357821943}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:17+02","p50":124.15826366666666,"p95":427.9907656928685,"p99":449.04125851229384}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:18+02","p50":114.9532918888889,"p95":448.636051890614,"p99":485.4835679474907}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:19+02","p50":176.6278854375,"p95":323.2559806472752,"p99":347.9221029315009}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:20+02","p50":211.88934416666666,"p95":262.22510196252955,"p99":281.2612729744716}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:21+02","p50":180.81894740625,"p95":259.6539730155451,"p99":269.67856666175607}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:22+02","p50":196.672637,"p95":266.94895766008,"p99":298.00997561267087}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:23+02","p50":164.28795509375,"p95":272.20733088255565,"p99":286.64984232324144}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:24+02","p50":239.44598994444445,"p95":422.5159794253372,"p99":439.2937172332916}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:25+02","p50":187.291075,"p95":354.77484739822967,"p99":374.8504402552736}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:26+02","p50":194.9652128333333,"p95":275.41904993178474,"p99":288.84490828162814}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:27+02","p50":203.21512822222223,"p95":355.13161239707694,"p99":395.83519843569616}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:28+02","p50":262.27167633333335,"p95":334.4411825664316,"p99":350.01384678881715}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:29+02","p50":194.2414280625,"p95":304.2947880739841,"p99":343.91379995135236}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:30+02","p50":172.834217,"p95":286.87646965032957,"p99":336.19378734832765}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:31+02","p50":207.0231061875,"p95":302.47231828407706,"p99":316.54491024100827}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:32+02","p50":181.0753525,"p95":321.49112572849896,"p99":351.5424148507652}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:33+02","p50":153.663604,"p95":276.54249616299137,"p99":293.4143785937378}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:34+02","p50":160.192512,"p95":392.5024005158302,"p99":406.7674115495229}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:35+02","p50":115.11713325,"p95":374.7461023176923,"p99":403.76463397014857}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:36+02","p50":195.5520289375,"p95":330.24640575101205,"p99":378.25412397826955}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:37+02","p50":153.22983206249998,"p95":302.1021002301149,"p99":320.31823634316964}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:38+02","p50":190.33183956249997,"p95":264.897034113108,"p99":280.9633527328908}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:39+02","p50":161.7138188125,"p95":297.7743485606594,"p99":329.2594311006222}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:40+02","p50":198.20748,"p95":377.3354283850393,"p99":406.5434730048401}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:41+02","p50":139.463968625,"p95":266.8608783056278,"p99":289.8984755721698}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:42+02","p50":114.82625178125,"p95":349.68077743478915,"p99":361.65627729169654}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:43+02","p50":232.9297346875,"p95":309.02823247616396,"p99":327.71930822933507}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:44+02","p50":146.7611834375,"p95":314.5849987406578,"p99":368.70549214709615}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:45+02","p50":133.7292851875,"p95":297.54178871003234,"p99":310.04929223846005}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:46+02","p50":140.45567866666667,"p95":412.6909459159718,"p99":451.2378967052231}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:47+02","p50":195.74258483333332,"p95":391.1413024729508,"p99":426.36363475451185}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:48+02","p50":141.246246,"p95":379.41185379152716,"p99":406.632193284086}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:49+02","p50":162.00057353125,"p95":288.8276731247837,"p99":313.6953214802961}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:50+02","p50":150.4389035,"p95":353.7256544370002,"p99":383.2435627905927}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:51+02","p50":175.6269825,"p95":248.15324239547587,"p99":264.5156661502189}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:52+02","p50":159.8990134375,"p95":300.3999955336074,"p99":325.6686536526949}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:53+02","p50":162.34384893749998,"p95":297.086694576544,"p99":332.67165895619155}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:54+02","p50":146.514717375,"p95":265.59541198054677,"p99":281.9044932644291}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:55+02","p50":175.333063,"p95":334.77658803869053,"p99":373.9112342957733}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:56+02","p50":131.2167323125,"p95":288.0913442904415,"p99":310.9476716580658}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:57+02","p50":146.7495225625,"p95":297.6914536560938,"p99":339.77493364830207}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:58+02","p50":142.8694296875,"p95":285.2685503283086,"p99":313.73204227613735}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:24:59+02","p50":206.041962,"p95":277.91586266952214,"p99":301.9713539372003}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:00+02","p50":187.0775275625,"p95":301.2439236612787,"p99":318.2949744250431}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:01+02","p50":116.769160625,"p95":297.3274914935353,"p99":314.41500567552447}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:02+02","p50":121.01627225,"p95":302.45192923908996,"p99":323.91664271936037}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:03+02","p50":115.9357485,"p95":291.0901811554011,"p99":310.4947344401798}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:04+02","p50":128.8123610625,"p95":293.8868525166878,"p99":309.0480242172456}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:05+02","p50":160.981729375,"p95":307.9032117912121,"p99":320.6469388115158}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:06+02","p50":147.9102275,"p95":278.95437096888554,"p99":299.85087856600524}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:07+02","p50":177.696945125,"p95":255.77304940730096,"p99":274.12012098093413}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:08+02","p50":145.12405118750002,"p95":309.2880586250639,"p99":335.62102451202105}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:09+02","p50":188.25798759375,"p95":291.11708460701226,"p99":316.63812602788687}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:10+02","p50":167.07433734375,"p95":276.3570106623106,"p99":298.2238179203718}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:11+02","p50":157.453457625,"p95":249.26331657150982,"p99":261.95071847950743}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:12+02","p50":170.7052875,"p95":272.21180737058626,"p99":291.06299620788957}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:13+02","p50":166.31834390625,"p95":255.44433066577483,"p99":272.64818770659184}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:14+02","p50":179.1684726875,"p95":257.67500678455974,"p99":283.6161977368152}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:15+02","p50":190.28375031250002,"p95":244.14229401287508,"p99":265.4086650401878}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:16+02","p50":194.85328783333333,"p95":268.6921016668321,"p99":336.8306007708764}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:17+02","p50":217.8591368888889,"p95":270.79396199034244,"p99":293.95661484732244}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:18+02","p50":194.134548,"p95":285.16345218963994,"p99":309.2768642459817}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:19+02","p50":178.6079338125,"p95":268.24652942969857,"p99":300.60436891284706}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:20+02","p50":161.261008,"p95":276.3407417475745,"p99":293.17805059296325}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:21+02","p50":172.40981234375,"p95":233.20667978189297,"p99":247.1107588331442}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:22+02","p50":188.83555416666664,"p95":297.4500083579718,"p99":307.55552874326133}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:23+02","p50":170.39420646874999,"p95":211.03686184797803,"p99":224.53489826566695}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:24+02","p50":164.35997515625002,"p95":276.49485311147856,"p99":292.59989314810065}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:25+02","p50":143.64633949999998,"p95":272.80621859268007,"p99":281.2128650680747}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:26+02","p50":149.58510009375,"p95":307.63088744441603,"p99":335.33292383728195}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:27+02","p50":168.15508775,"p95":285.7666212016733,"p99":313.92114463868904}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:28+02","p50":188.28659266666668,"p95":295.94769378776715,"p99":315.91148610047435}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:29+02","p50":178.75765806249998,"p95":295.6567288852167,"p99":317.2789837458754}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:30+02","p50":152.27584396875,"p95":341.79098242099843,"p99":352.3200402101381}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:31+02","p50":163.94469566666666,"p95":323.94278911006387,"p99":347.9313984693055}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:32+02","p50":190.45950209375002,"p95":295.96613964145297,"p99":314.3702196151154}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:33+02","p50":180.63572125000002,"p95":284.8222163975665,"p99":318.613081566169}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:34+02","p50":188.64411037500003,"p95":266.3229787653223,"p99":286.69827406677865}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:35+02","p50":184.52384700000002,"p95":263.67790348002853,"p99":303.49705895792914}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:36+02","p50":179.47607453125,"p95":355.8448701697182,"p99":382.9379842353823}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:37+02","p50":181.09762290625002,"p95":251.98523698942577,"p99":268.9887313744817}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:38+02","p50":129.08299715625,"p95":305.38149777157025,"p99":339.56128032651185}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:39+02","p50":173.939551375,"p95":295.13943168028476,"p99":306.4496784014864}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:40+02","p50":184.15993678125,"p95":219.87376809752035,"p99":242.36668579811266}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:41+02","p50":176.92126165624998,"p95":255.3895744386714,"p99":273.5941948558238}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:42+02","p50":184.09578978125,"p95":242.15070153322625,"p99":255.75301165470577}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:43+02","p50":188.93190390625,"p95":270.17291396094987,"p99":288.7374878324158}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:44+02","p50":170.27386246875,"p95":302.0877696634417,"p99":322.2277050531044}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:45+02","p50":171.28439425,"p95":294.0131721756791,"p99":312.53864885021994}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:46+02","p50":216.75257355555553,"p95":322.79357449730196,"p99":356.2870012329533}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:47+02","p50":195.4499258333333,"p95":390.5324061168661,"p99":428.31820336249876}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:48+02","p50":237.23479605555553,"p95":356.1376857394992,"p99":380.66792645569654}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:49+02","p50":198.9062845,"p95":303.59157814383747,"p99":335.63319875815586}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:50+02","p50":197.55178744444444,"p95":246.66201525187805,"p99":286.0360022681031}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:51+02","p50":203.1253768125,"p95":298.65952319879835,"p99":323.86093111381956}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:52+02","p50":157.29233521875,"p95":317.64778697795776,"p99":350.21748591659593}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:53+02","p50":162.37203615625,"p95":305.0249680160875,"p99":344.69489125115444}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:54+02","p50":174.49744640625,"p95":279.51049966483635,"p99":299.0728399718125}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:55+02","p50":155.20962281250002,"p95":267.78474178160235,"p99":295.7095052904222}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:56+02","p50":157.371921875,"p95":293.56901468093776,"p99":319.46319449705885}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:57+02","p50":157.63671618749999,"p95":280.69837198581314,"p99":305.63588144813065}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:58+02","p50":169.2675446875,"p95":288.29848496026744,"p99":320.18566677379846}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:25:59+02","p50":173.090842,"p95":295.79925527414963,"p99":309.57774177467587}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:00+02","p50":206.27317477777777,"p95":332.6504074051008,"p99":364.18589949073026}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:01+02","p50":182.27632059375,"p95":221.51328541887975,"p99":266.13306615993787}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:02+02","p50":168.33315818749998,"p95":233.01725007892423,"p99":262.8184382386236}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:03+02","p50":172.41379237499999,"p95":247.44395826688336,"p99":268.36499833924194}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:04+02","p50":174.61597949999998,"p95":271.83778823314736,"p99":291.0804720828}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:05+02","p50":187.59158225,"p95":282.9914468398229,"p99":309.3119564544549}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:06+02","p50":181.2318166875,"p95":251.55728365982807,"p99":268.7432143299179}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:07+02","p50":172.82143659375,"p95":270.6421756053556,"p99":301.884456831074}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:08+02","p50":172.9038281875,"p95":251.8655380025793,"p99":298.0483943289444}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:09+02","p50":176.55819175,"p95":243.4837930472659,"p99":266.464914060235}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:10+02","p50":176.31888506250002,"p95":281.58976408667183,"p99":308.106734991333}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:11+02","p50":174.0874495,"p95":221.15128212171365,"p99":235.78570532621}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:12+02","p50":176.74896453125,"p95":249.94871128860984,"p99":268.7346848409734}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:13+02","p50":179.19973940625,"p95":322.88291914086074,"p99":346.68211576098633}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:14+02","p50":167.114223,"p95":279.35395499266207,"p99":304.0353890021672}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:15+02","p50":189.64666166666666,"p95":296.94451521529624,"p99":339.07841855854224}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:16+02","p50":182.53102394444443,"p95":292.5580229132622,"p99":311.0710275786543}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:17+02","p50":215.63680366666665,"p95":353.435264173954,"p99":391.11029739471434}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:18+02","p50":199.57578966666668,"p95":283.7855816993978,"p99":325.47797826733233}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:19+02","p50":180.462869625,"p95":294.70932847846296,"p99":353.2521015840626}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:20+02","p50":171.41940599999998,"p95":277.59080925117195,"p99":308.29028416486597}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:21+02","p50":175.43739625,"p95":255.71843944816638,"p99":288.13309434937094}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:22+02","p50":155.1737825625,"p95":319.33560243206887,"p99":352.25093038751055}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:23+02","p50":145.570873375,"p95":296.3754123819015,"p99":317.5755562688322}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:24+02","p50":160.4218194375,"p95":340.7114007824508,"p99":364.4656010268531}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:25+02","p50":162.16974831250002,"p95":279.9444344233162,"p99":296.29224127451107}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:26+02","p50":145.263541375,"p95":303.46550683899375,"p99":317.1922718414955}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:27+02","p50":171.87917687499998,"p95":297.13505856224157,"p99":326.78417222842313}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:28+02","p50":131.22916377777779,"p95":359.898864760616,"p99":376.2556717880466}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:29+02","p50":119.6028695,"p95":364.6834245433574,"p99":392.0918980344329}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:30+02","p50":152.96088740625,"p95":328.00408136766305,"p99":351.2401286689439}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:31+02","p50":102.28407511111112,"p95":460.5637817656824,"p99":478.0751268420224}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:32+02","p50":126.57270088888889,"p95":360.61222046122094,"p99":371.3287526978929}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:33+02","p50":132.26185315625,"p95":393.72701131120203,"p99":408.43086990150164}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:34+02","p50":165.71685333333335,"p95":356.6648353605751,"p99":384.9428702928367}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:35+02","p50":144.32326233333333,"p95":325.38959274268797,"p99":355.6178618845525}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:36+02","p50":141.32919166666667,"p95":354.349809254661,"p99":378.2323980121696}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:37+02","p50":129.520349,"p95":365.91650590921705,"p99":388.41206783638785}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:38+02","p50":107.45927116666667,"p95":388.48086245010336,"p99":410.1962572701168}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:39+02","p50":99.57383877777777,"p95":416.093772003908,"p99":424.19737575689317}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:40+02","p50":103.85079288888888,"p95":426.45169877547954,"p99":433.7994967553639}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:41+02","p50":106.83163511111111,"p95":423.48536041654,"p99":458.9282701152256}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:42+02","p50":139.0369598888889,"p95":367.1590390966934,"p99":378.2780998659148}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:43+02","p50":150.3051636666667,"p95":352.1733739595711,"p99":370.4108028213062}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:44+02","p50":183.41215333333332,"p95":366.32920334733535,"p99":389.2896098950312}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:45+02","p50":119.20249233333334,"p95":342.0096206443362,"p99":361.2770049820531}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:46+02","p50":142.06372866666666,"p95":431.9168797226615,"p99":453.44524299833773}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:47+02","p50":166.04477699999998,"p95":494.36011340903895,"p99":528.4672684806739}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:48+02","p50":243.59032388888886,"p95":367.2906143898193,"p99":396.595394825233}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:49+02","p50":172.526195,"p95":356.46564384029017,"p99":398.40816489110944}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:50+02","p50":156.869940375,"p95":270.63714209433107,"p99":298.84650663945007}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:51+02","p50":184.6008940625,"p95":302.04423156519533,"p99":330.38419290871815}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:52+02","p50":136.397303,"p95":348.60078693502504,"p99":379.10041040987636}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:53+02","p50":208.5958850625,"p95":297.4139880978174,"p99":342.4072156456852}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:54+02","p50":167.1972085,"p95":339.2144632359161,"p99":357.6255409016114}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:55+02","p50":200.5229682222222,"p95":284.90606338766986,"p99":323.40569906101587}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:56+02","p50":210.01563066666668,"p95":338.4560215353322,"p99":359.1871781713526}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:57+02","p50":179.1637526875,"p95":335.4905377814721,"p99":400.75122673833704}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:58+02","p50":189.334459,"p95":289.19623811589145,"p99":322.82845078143214}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:26:59+02","p50":197.29853,"p95":254.67813269819308,"p99":286.1629846492476}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:00+02","p50":190.4065411875,"p95":284.5076546309163,"p99":331.9035186802502}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:01+02","p50":181.47052406249998,"p95":279.69881130609434,"p99":313.719389493555}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:02+02","p50":151.94579166666668,"p95":354.61666319084213,"p99":371.7136036657352}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:03+02","p50":166.281235,"p95":386.73531692902384,"p99":420.649655327538}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:04+02","p50":148.7445223125,"p95":359.46318257351254,"p99":376.27688912959866}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:05+02","p50":184.21798096875,"p95":277.70900109177575,"p99":296.8218056136756}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:06+02","p50":162.87833112500002,"p95":314.527402146051,"p99":339.50987744527913}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:07+02","p50":168.53892825,"p95":306.9428177077141,"p99":334.97422145559887}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:08+02","p50":192.90900859375,"p95":285.82773883297716,"p99":341.94940182323097}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:09+02","p50":194.55982674999998,"p95":246.99063782974244,"p99":280.4351090385742}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:10+02","p50":174.28953725,"p95":247.9744066636477,"p99":279.17424074895763}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:11+02","p50":193.928404125,"p95":253.76260802380764,"p99":284.1286567328668}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:12+02","p50":189.29576181250002,"p95":263.7371795551295,"p99":278.0667329535737}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:13+02","p50":182.957293,"p95":294.6086662118673,"p99":317.8614731482558}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:14+02","p50":203.753277875,"p95":281.0476899822991,"p99":316.78528554933285}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:15+02","p50":180.89417,"p95":234.47657934764052,"p99":263.55548825027086}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:16+02","p50":203.712888,"p95":323.5563643740608,"p99":364.48719365641284}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:17+02","p50":260.81837955555557,"p95":380.940437108066,"p99":410.5779094341323}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:18+02","p50":198.35689022222223,"p95":365.16697414600145,"p99":396.2802318050656}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:19+02","p50":174.5708140625,"p95":285.0906010951182,"p99":328.2365876712575}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:20+02","p50":181.09996243749998,"p95":276.39923109533675,"p99":315.6490332285991}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:21+02","p50":186.5207919375,"p95":270.1056047462885,"p99":321.6402500349741}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:22+02","p50":184.4209691875,"p95":246.49267304373691,"p99":269.07445565216636}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:23+02","p50":193.4719618888889,"p95":273.3790767838836,"p99":314.5124505561542}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:24+02","p50":186.08233903125,"p95":266.5522896144972,"p99":304.5487637141478}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:25+02","p50":172.57913712500002,"p95":253.0352081657167,"p99":262.7493767693462}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:26+02","p50":178.66033675,"p95":310.59731244291976,"p99":344.55369092844967}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:27+02","p50":188.10271955555552,"p95":270.31915788613077,"p99":356.1369984561987}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:28+02","p50":159.21614743749998,"p95":287.4595513562077,"p99":316.61637117335323}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:29+02","p50":182.15573256250002,"p95":246.1517650496707,"p99":281.4713636001492}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:30+02","p50":173.9616719375,"p95":238.7210853960729,"p99":269.03982139480013}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:31+02","p50":176.48987084375,"p95":269.9758206322698,"p99":310.2472167075813}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:32+02","p50":169.6217251875,"p95":334.4223009461551,"p99":381.64384746658703}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:33+02","p50":173.198981,"p95":295.5843631533242,"p99":322.93146719270993}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:34+02","p50":162.326174875,"p95":309.13122795573594,"p99":349.75819565876577}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:35+02","p50":195.27382344444445,"p95":309.3739377559875,"p99":349.0708461076501}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:36+02","p50":181.84496800000002,"p95":338.0018862877129,"p99":369.45319263241146}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:37+02","p50":179.28821343749996,"p95":307.6492964786487,"p99":347.9981545432668}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:38+02","p50":183.731094,"p95":314.62089908723397,"p99":346.75015494358547}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:39+02","p50":194.126655625,"p95":260.54428832613536,"p99":295.5964730170975}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:40+02","p50":177.77776921875,"p95":288.4279057673874,"p99":329.3559257648399}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:41+02","p50":177.5621809375,"p95":305.15425345334114,"p99":376.9097950054536}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:42+02","p50":211.76522111111112,"p95":357.18577438551364,"p99":405.6006579650128}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:43+02","p50":184.48686756249998,"p95":255.88449723447326,"p99":300.79711599596027}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:44+02","p50":184.867052125,"p95":257.1018845733412,"p99":351.9285792595954}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:45+02","p50":177.895700875,"p95":254.4358383973141,"p99":303.02956470006563}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:46+02","p50":172.86840555555557,"p95":338.72827478282875,"p99":394.7819297608509}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:47+02","p50":254.53267066666663,"p95":380.7524738069238,"p99":407.6565277409205}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:48+02","p50":234.29611466666668,"p95":359.137765516373,"p99":411.1764193704636}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:49+02","p50":225.3586666111111,"p95":343.9276526965337,"p99":428.88149968406293}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:50+02","p50":172.047994125,"p95":265.0060092756381,"p99":308.71778692960356}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:51+02","p50":176.81159381249998,"p95":266.21200436137303,"p99":303.04123896559526}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:52+02","p50":181.7798923125,"p95":285.306894141206,"p99":359.1930122079897}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:53+02","p50":178.96946090625002,"p95":271.8641297672259,"p99":308.8865002192125}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:54+02","p50":187.8267409375,"p95":294.422145442864,"p99":313.28276309497954}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:55+02","p50":181.83900615624998,"p95":282.85299389284353,"p99":414.0699751253739}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:56+02","p50":175.685083625,"p95":305.89563840037823,"p99":392.00051498221444}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:57+02","p50":181.27693875,"p95":283.32694764093947,"p99":371.5354144100413}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:58+02","p50":189.2878153125,"p95":303.2108947008197,"p99":338.6312464663994}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:27:59+02","p50":184.22582406249998,"p95":264.0269759026263,"p99":287.74213224001693}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:00+02","p50":174.34488290624998,"p95":277.9093237921628,"p99":306.53292767350075}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:01+02","p50":172.891099125,"p95":313.25551782171726,"p99":370.5475053258922}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:02+02","p50":173.47537109375,"p95":267.0225890905224,"p99":326.83412361638716}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:03+02","p50":180.10718253125,"p95":256.32498419858223,"p99":370.94424462468885}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:04+02","p50":185.4720615625,"p95":273.6668380397305,"p99":301.26450008935115}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:05+02","p50":183.60832521875,"p95":295.63168707886484,"p99":313.20009102869676}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:06+02","p50":188.33244009375,"p95":292.2807335878806,"p99":330.34957164277387}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:07+02","p50":175.0624861111111,"p95":282.2448508753879,"p99":371.1998730442781}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:08+02","p50":193.189431375,"p95":289.6453383929767,"p99":385.4908967383757}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:09+02","p50":177.4467085,"p95":270.8755065380138,"p99":305.5201443798356}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:10+02","p50":183.44905834374998,"p95":281.39212976917014,"p99":356.07378722806266}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:11+02","p50":180.18716506250001,"p95":297.3179076449388,"p99":381.84803981396294}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:12+02","p50":179.81955490625,"p95":273.71195399226775,"p99":395.0524973836839}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:13+02","p50":197.876847,"p95":304.39508602396387,"p99":410.64733257506373}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:14+02","p50":204.00739866666666,"p95":345.92283645437016,"p99":428.4246764596453}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:15+02","p50":186.27291156249998,"p95":317.68894601801037,"p99":372.11997879711726}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:16+02","p50":195.23936694444447,"p95":313.743992693481,"p99":384.4459505873584}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:17+02","p50":231.78869322222224,"p95":395.5796082287531,"p99":437.190137809721}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:18+02","p50":209.29811977777777,"p95":387.20049555944814,"p99":530.0324769997549}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:19+02","p50":178.382816375,"p95":283.33926716237164,"p99":372.69932674834064}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:20+02","p50":175.56871912500003,"p95":285.42811584741463,"p99":339.69615951487253}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:21+02","p50":182.872967125,"p95":284.1898924847056,"p99":361.01803691475106}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:22+02","p50":179.582868125,"p95":293.5345978478228,"p99":361.35547253136446}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:23+02","p50":178.9781014375,"p95":288.1660969617573,"p99":321.85255615265464}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:24+02","p50":179.80794075,"p95":274.03609601131484,"p99":309.22765644909475}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:25+02","p50":167.9727905,"p95":282.9057814949338,"p99":353.0839556728606}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:26+02","p50":168.2010754375,"p95":290.81359906178733,"p99":353.23707309792997}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:27+02","p50":178.9828831875,"p95":303.51690530413777,"p99":345.44209892959077}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:28+02","p50":179.24066525,"p95":283.17939501104735,"p99":322.1220969011841}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:29+02","p50":182.18125053125,"p95":278.39569861031396,"p99":328.2225490846484}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:30+02","p50":168.5792554375,"p95":280.10370993942666,"p99":339.67714086049267}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:31+02","p50":185.55199322222222,"p95":290.30181307020194,"p99":325.63648723553825}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:32+02","p50":183.94873153124996,"p95":291.04865962262016,"p99":364.07572688195205}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:33+02","p50":175.4366629375,"p95":319.27942885836194,"p99":451.61856201362133}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:34+02","p50":183.21118928125,"p95":304.4462418187314,"p99":392.6879494052341}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:35+02","p50":181.02044783333335,"p95":277.3016030049224,"p99":327.82674934820557}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:36+02","p50":191.68535487499997,"p95":309.53458136322985,"p99":385.6664749968862}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:37+02","p50":181.94860699999998,"p95":285.48172275946047,"p99":345.92694605340574}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:38+02","p50":177.33195643750003,"p95":280.75634856703374,"p99":320.6769525189438}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:39+02","p50":184.9492619375,"p95":285.2504567660826,"p99":358.27477145018486}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:40+02","p50":183.04699634375,"p95":273.8091363463696,"p99":336.4280685615406}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:41+02","p50":173.40073978125002,"p95":299.81940583663135,"p99":362.2512695265634}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:42+02","p50":192.40045434374997,"p95":288.41179595461534,"p99":392.5507122601442}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:43+02","p50":178.7790755,"p95":292.0326294655233,"p99":371.9958734281445}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:44+02","p50":191.67839765624998,"p95":300.24229647855213,"p99":381.395401676014}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:45+02","p50":187.67865516666666,"p95":275.18243718778587,"p99":315.0424383924927}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:46+02","p50":197.05481855555558,"p95":371.42625160446977,"p99":467.6787794722595}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:47+02","p50":240.87116833333334,"p95":391.572233295926,"p99":519.9037980597662}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:48+02","p50":235.87759300000002,"p95":409.31382142708264,"p99":456.40398974086264}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:49+02","p50":224.59980644444443,"p95":355.4837907680993,"p99":423.4581333588619}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:50+02","p50":203.3833103333333,"p95":359.28066545660334,"p99":411.95622871698185}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:51+02","p50":173.208011,"p95":307.059043303128,"p99":387.6606016126637}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:52+02","p50":192.56502483333335,"p95":339.2739105763652,"p99":392.4409275000506}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:53+02","p50":199.82310011111113,"p95":297.42499997561225,"p99":345.9870140928783}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:54+02","p50":188.8862,"p95":322.0846949810638,"p99":378.0526086578979}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:55+02","p50":180.85722199999998,"p95":288.82187167933654,"p99":343.70991487231447}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:56+02","p50":185.458497125,"p95":284.31283382909396,"p99":335.0110088045254}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:57+02","p50":173.795028,"p95":279.8780628272852,"p99":339.887694766423}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:58+02","p50":174.3704303125,"p95":284.08356637899925,"p99":340.7857195103054}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:28:59+02","p50":188.94265933333335,"p95":296.63605886391923,"p99":361.7627259493022}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:00+02","p50":180.5411283888889,"p95":361.76556212719953,"p99":524.4916457930836}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:01+02","p50":186.18883196875,"p95":274.26070087586845,"p99":342.69958166062355}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:02+02","p50":187.39715016666665,"p95":338.78737315855227,"p99":428.3477456998158}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:03+02","p50":180.85751015625,"p95":310.2928600412392,"p99":383.3905066506598}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:04+02","p50":191.18199811111108,"p95":319.8406756083638,"p99":389.0285095677936}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:05+02","p50":175.32893340625003,"p95":286.3750313720981,"p99":349.6345958194578}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:06+02","p50":183.2386705,"p95":286.19398265717626,"p99":345.68618192171385}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:07+02","p50":192.226055,"p95":297.8554963492075,"p99":387.10925219785975}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:08+02","p50":182.3250955,"p95":294.3484677958275,"p99":363.44833844165186}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:09+02","p50":183.173839125,"p95":283.8246264684485,"p99":396.3395074730677}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:10+02","p50":187.69375688888888,"p95":291.33825403262443,"p99":346.2634405867634}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:11+02","p50":190.76420799999997,"p95":298.0675107736571,"p99":369.94269045167687}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:12+02","p50":185.63644483333334,"p95":292.96604209052634,"p99":348.131268917387}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:13+02","p50":190.84302672222222,"p95":304.11442938740254,"p99":348.86566499752047}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:14+02","p50":199.46363427777774,"p95":310.30567044360345,"p99":378.27760563399363}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:15+02","p50":195.37079677777777,"p95":342.68533850806176,"p99":435.84184006633376}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:16+02","p50":227.98423083333333,"p95":371.98305139658953,"p99":452.75762399102973}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:17+02","p50":206.52422533333333,"p95":348.3823036163349,"p99":436.44131284798}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:18+02","p50":222.68072266666664,"p95":441.58051954525274,"p99":477.3179562999191}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:19+02","p50":209.60902766666666,"p95":427.4816595137386,"p99":490.64022842250347}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:20+02","p50":195.28943727777778,"p95":386.7631437744287,"p99":528.5339059680829}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:21+02","p50":177.19292612499999,"p95":296.9966029884962,"p99":351.0876559623608}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:22+02","p50":187.82212983333332,"p95":320.7605695487688,"p99":356.4046808374877}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:23+02","p50":200.91790177777776,"p95":325.3274707046466,"p99":382.9400430357437}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:24+02","p50":192.095791,"p95":305.1662079537307,"p99":368.2749778338752}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:25+02","p50":196.35827400000002,"p95":353.0490556321643,"p99":452.9795076071234}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:26+02","p50":201.5129793333333,"p95":313.126704331702,"p99":380.70473433486416}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:27+02","p50":191.16391925,"p95":322.38616867259213,"p99":383.38376561450195}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:28+02","p50":189.79404555555558,"p95":318.48205436716233,"p99":403.98749458813404}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:29+02","p50":202.74254166666665,"p95":294.48037518920864,"p99":337.5341222375772}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:30+02","p50":200.895257,"p95":328.09237700937075,"p99":385.4590321867692}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:31+02","p50":195.1853305,"p95":317.0557495971711,"p99":341.8695390660572}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:32+02","p50":185.53336349999998,"p95":313.77426831526185,"p99":349.27972168170163}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:33+02","p50":185.72398405555555,"p95":311.7927280365346,"p99":385.93252687193103}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:34+02","p50":196.32199388888887,"p95":313.3746737635519,"p99":397.15976756807805}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:35+02","p50":195.77611466666667,"p95":318.6987151699731,"p99":391.9965806837673}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:36+02","p50":205.0182351666667,"p95":339.42000078268126,"p99":413.6267370639348}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:37+02","p50":194.602442,"p95":290.1181957805015,"p99":318.45405940259076}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:38+02","p50":191.95687622222223,"p95":297.9188392002153,"p99":600.743555807945}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:39+02","p50":205.7433178888889,"p95":306.8241684115159,"p99":417.0625454678657}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:40+02","p50":203.63462788888887,"p95":308.77570929680644,"p99":403.33060484419275}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:41+02","p50":188.15050315625,"p95":341.6003651119405,"p99":389.12308656003614}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:42+02","p50":193.08033874999998,"p95":293.74547187106015,"p99":362.63748073884443}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:43+02","p50":205.56052744444446,"p95":347.56138723170784,"p99":410.31080260947607}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:44+02","p50":229.83359483333334,"p95":389.4037414396386,"p99":503.9098959869328}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:45+02","p50":196.40565455555557,"p95":340.76758979425955,"p99":415.5161074771428}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:46+02","p50":204.35345344444445,"p95":377.3570702471015,"p99":426.65224602999325}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:47+02","p50":225.82986166666666,"p95":396.9132368823718,"p99":479.39892228121164}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:48+02","p50":236.2227238888889,"p95":420.8084648244519,"p99":585.2791829253792}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:49+02","p50":240.21362611111113,"p95":414.5261726602379,"p99":504.4608610409298}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:50+02","p50":179.1758516875,"p95":311.6895966241861,"p99":416.06294661549447}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:51+02","p50":258.72182475,"p95":449.94842317300106,"p99":494.3432882395859}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:52+02","p50":237.80108577777779,"p95":442.9910982815889,"p99":543.2034437793208}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:53+02","p50":195.665854,"p95":310.5751608634274,"p99":357.82289057198335}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:54+02","p50":196.82895044444444,"p95":319.5017947959468,"p99":373.76577025522687}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:55+02","p50":200.800851,"p95":342.9027650422167,"p99":400.36503775353503}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:56+02","p50":182.81177111111114,"p95":330.3610955594633,"p99":375.26893745562074}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:57+02","p50":196.06131533333334,"p95":315.32270442228736,"p99":392.0299460193381}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:58+02","p50":194.06245544444445,"p95":364.67851196668533,"p99":415.71667057423065}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:29:59+02","p50":191.181879,"p95":322.84256489709725,"p99":388.43254446045876}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:00+02","p50":215.1314085,"p95":322.17242880714775,"p99":391.6174805017881}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:01+02","p50":195.36236833333336,"p95":320.0050312237211,"p99":356.24980559045576}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:02+02","p50":191.07694611111114,"p95":294.1568991743737,"p99":355.2651012445986}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:03+02","p50":194.71243922222223,"p95":320.18561128697405,"p99":380.92638734178587}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:04+02","p50":202.7954959444444,"p95":359.1465304480229,"p99":422.0879062050114}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:05+02","p50":204.89305344444446,"p95":328.98988321948696,"p99":443.6920679938033}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:06+02","p50":189.607352,"p95":308.7473520167389,"p99":362.31522194940186}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:07+02","p50":197.15711483333334,"p95":305.333828006937,"p99":402.04787657486725}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:08+02","p50":205.9379448888889,"p95":321.8561829285126,"p99":390.4513909561615}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:09+02","p50":200.68760466666666,"p95":297.40365169285434,"p99":377.10381864853736}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:10+02","p50":205.77781333333334,"p95":329.51096899003556,"p99":362.6896775728693}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:11+02","p50":194.04743865624997,"p95":308.2045224999106,"p99":370.28523751307915}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:12+02","p50":187.54421494444443,"p95":304.8126183170655,"p99":374.488265570702}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:13+02","p50":207.95467661111113,"p95":353.32314698984675,"p99":425.3301876491327}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:14+02","p50":204.24703350000001,"p95":335.6742798255293,"p99":385.04407642636494}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:15+02","p50":199.99838666666665,"p95":343.40152375740155,"p99":414.83322244828344}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:16+02","p50":215.66121344444446,"p95":389.6706946554674,"p99":483.6929230183053}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:17+02","p50":252.786444,"p95":392.7790993085299,"p99":468.75099819384195}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:18+02","p50":250.53043150000002,"p95":484.7992541378441,"p99":597.2657474953403}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:19+02","p50":211.7113725,"p95":363.0630051821494,"p99":439.0834621645174}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:20+02","p50":196.90273866666666,"p95":327.408693076395,"p99":409.49469937997054}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:21+02","p50":195.202466,"p95":332.31202677364854,"p99":392.09884465544843}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:22+02","p50":207.1027911111111,"p95":318.4890294689935,"p99":383.3717451876423}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:23+02","p50":194.8952988888889,"p95":336.54343823294545,"p99":379.69921173835564}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:24+02","p50":203.22173966666665,"p95":325.858825276369,"p99":375.65041018047714}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:25+02","p50":208.34684211111116,"p95":321.4584466321313,"p99":375.1542131777668}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:26+02","p50":208.5398351111111,"p95":333.55307064500647,"p99":414.4640012401466}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:27+02","p50":207.8972022222222,"p95":313.9749510282942,"p99":365.0789934250443}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:28+02","p50":207.131656,"p95":325.7565959555035,"p99":381.7642503274655}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:29+02","p50":199.86096222222224,"p95":352.385276092414,"p99":457.95726721027086}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:30+02","p50":203.449391,"p95":311.8331262542561,"p99":368.2060998664541}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:31+02","p50":201.434107,"p95":323.3725637632765,"p99":412.7679559767577}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:32+02","p50":195.92857949999998,"p95":331.18196339855,"p99":350.3222537354736}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:33+02","p50":209.50369350000003,"p95":340.38541028134443,"p99":403.06620992008783}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:34+02","p50":204.23807227777777,"p95":332.1988236080222,"p99":374.7636895254688}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:35+02","p50":208.2409052222222,"p95":314.55598781393724,"p99":361.87513171565223}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:36+02","p50":199.80649922222221,"p95":346.9903820318022,"p99":420.8994297507582}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:37+02","p50":202.18685877777776,"p95":344.1347232513341,"p99":385.2447149118266}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:38+02","p50":199.01452222222224,"p95":314.9894906094337,"p99":382.90295159783483}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:39+02","p50":197.9540558888889,"p95":298.5944473896455,"p99":361.7452332461603}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:40+02","p50":193.04107544444446,"p95":336.9445636221,"p99":439.2198271483893}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:41+02","p50":204.85928644444445,"p95":329.6950419662266,"p99":376.36683230102875}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:42+02","p50":202.08633,"p95":335.5709988721693,"p99":449.73176842874045}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:43+02","p50":204.33184033333336,"p95":341.681939853343,"p99":398.2243132224665}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:44+02","p50":216.2977343888889,"p95":338.53055366873934,"p99":403.5138837327957}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:45+02","p50":210.7147877222222,"p95":349.652755861713,"p99":411.895023365963}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:46+02","p50":234.20458283333335,"p95":420.9659186840737,"p99":465.7731906653318}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:47+02","p50":293.1767015,"p95":474.4596434355699,"p99":539.9835101593533}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:48+02","p50":264.5489315,"p95":488.1557553457098,"p99":638.3500447652283}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:49+02","p50":249.61188066666668,"p95":454.0098626703217,"p99":509.3023160842629}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:50+02","p50":210.4459478888889,"p95":310.4518044317118,"p99":393.9216711000547}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:51+02","p50":207.11965961111113,"p95":291.8754875246973,"p99":377.99808943608855}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:52+02","p50":203.21053294444445,"p95":319.5771978321209,"p99":370.6410599894028}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:53+02","p50":201.05646166666668,"p95":326.7955876665544,"p99":370.4877728239765}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:54+02","p50":211.28015066666663,"p95":344.0611608850927,"p99":390.8962635882895}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:55+02","p50":203.21056233333334,"p95":322.7953190818432,"p99":399.29807289031123}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:56+02","p50":194.33037533333334,"p95":326.5859764551394,"p99":410.8137740690634}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:57+02","p50":196.94781300000002,"p95":349.0178860734527,"p99":383.11062646083474}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:58+02","p50":204.45741644444445,"p95":327.84456876539326,"p99":381.39637339495084}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:30:59+02","p50":202.78864411111113,"p95":343.9740485821655,"p99":432.9572875947094}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:00+02","p50":205.95223866666666,"p95":335.41241242368073,"p99":387.34601652950363}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:01+02","p50":211.46082533333333,"p95":360.6423262300987,"p99":440.72140459379574}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:02+02","p50":197.3168747777778,"p95":329.8497866886737,"p99":374.03593301870967}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:03+02","p50":201.06334833333335,"p95":332.5075342838344,"p99":389.4220417752116}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:04+02","p50":202.27969394444446,"p95":353.7058692879639,"p99":457.29652491950606}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:05+02","p50":213.40677316666665,"p95":353.859651326473,"p99":433.6958857299137}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:06+02","p50":202.95112300000002,"p95":353.25017717877125,"p99":402.259600662328}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:07+02","p50":202.82333622222222,"p95":378.725918430907,"p99":453.3172039213264}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:08+02","p50":205.31515066666665,"p95":331.4173666829703,"p99":356.3674236977348}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:09+02","p50":199.81312433333335,"p95":332.1951719743659,"p99":372.910739085881}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:10+02","p50":212.85129116666667,"p95":329.4328011459733,"p99":419.0517945991316}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:11+02","p50":208.479479,"p95":336.37588835243065,"p99":443.67065954572126}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:12+02","p50":201.95191027777778,"p95":356.8024198754394,"p99":470.981126758841}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:13+02","p50":214.89258866666668,"p95":362.81409435534954,"p99":465.5712323945394}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:14+02","p50":214.901326,"p95":352.42506585925935,"p99":463.7408573168888}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:15+02","p50":211.19516933333333,"p95":349.87133545118365,"p99":430.80135017974516}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:16+02","p50":216.68483155555555,"p95":427.9566579194666,"p99":503.48659728901697}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:17+02","p50":271.41614725,"p95":441.15044961888987,"p99":512.1111401653092}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:18+02","p50":306.50639475,"p95":569.0558931402464,"p99":688.2556815319238}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:19+02","p50":254.88855933333332,"p95":476.27378646646827,"p99":597.141738387}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:20+02","p50":198.59727344444445,"p95":320.7389186073849,"p99":421.7967471746555}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:21+02","p50":206.5006558888889,"p95":342.536433827621,"p99":431.1285347640934}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:22+02","p50":217.01676633333332,"p95":314.94728825959515,"p99":373.5243463510456}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:23+02","p50":219.68843355555555,"p95":323.98048004122876,"p99":431.36936606665995}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:24+02","p50":208.99038966666663,"p95":384.34630963680394,"p99":467.5369988175614}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:25+02","p50":236.496023,"p95":384.22419352286676,"p99":427.69464637662}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:26+02","p50":206.78477172222222,"p95":338.1532447306398,"p99":391.5990644091611}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:27+02","p50":215.75523866666668,"p95":347.49228730711417,"p99":382.6851442212737}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:28+02","p50":225.08358466666667,"p95":360.465705259843,"p99":460.8789688364668}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:29+02","p50":218.99032866666667,"p95":374.5318866430343,"p99":449.60486558010365}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:30+02","p50":217.46841788888887,"p95":353.05215357354984,"p99":441.3271211384466}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:31+02","p50":209.3592971666667,"p95":333.3041163535206,"p99":430.5565161539979}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:32+02","p50":218.3212752222222,"p95":373.2606719534786,"p99":451.42541734322}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:33+02","p50":222.234025,"p95":397.0059874037057,"p99":793.392067381136}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:34+02","p50":217.33779122222222,"p95":373.3611613553316,"p99":464.0301309402456}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:35+02","p50":216.2161926111111,"p95":368.94490332340814,"p99":417.49581946537967}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:36+02","p50":213.15242594444445,"p95":375.2710341566048,"p99":455.5329708254852}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:37+02","p50":218.75487983333332,"p95":375.00843727908887,"p99":437.8240498166828}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:38+02","p50":202.4453447777778,"p95":343.02197042780926,"p99":403.13196793447014}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:39+02","p50":204.0129807222222,"p95":328.8427881609154,"p99":386.41570954950714}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:40+02","p50":211.9851253333333,"p95":334.1692656585097,"p99":380.46787664077857}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:41+02","p50":211.3932218888889,"p95":332.68919332244724,"p99":368.890662632473}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:42+02","p50":213.8660166666667,"p95":362.8466214451796,"p99":434.8911987468617}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:43+02","p50":217.3899536111111,"p95":346.0583209492775,"p99":458.01952588379385}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:44+02","p50":212.36583444444446,"p95":436.4938474673834,"p99":497.11531839578294}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:45+02","p50":223.41937505555555,"p95":369.2475102060952,"p99":395.1562643494711}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:46+02","p50":233.03533766666663,"p95":476.76958996716024,"p99":545.7290004457728}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:47+02","p50":247.136315,"p95":426.3290815725408,"p99":556.8003014434757}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:48+02","p50":267.027059375,"p95":441.90775357704644,"p99":500.54315025215794}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:49+02","p50":278.28536125000005,"p95":540.1597657444663,"p99":589.0782165439439}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:50+02","p50":255.21872077777778,"p95":459.0029476219091,"p99":563.1064010536222}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:51+02","p50":210.07645122222223,"p95":348.043192212614,"p99":433.9214216951654}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:52+02","p50":218.8208072777778,"p95":369.6221431672909,"p99":449.7027166413131}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:53+02","p50":227.08810566666668,"p95":367.7262492031312,"p99":440.15627429259945}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:54+02","p50":220.03400722222224,"p95":389.7109632064009,"p99":471.74365632854847}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:55+02","p50":217.39400366666666,"p95":369.9641647675552,"p99":412.9882192422233}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:56+02","p50":223.94330666666667,"p95":353.05924872795214,"p99":410.5398413230288}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:57+02","p50":203.63400333333334,"p95":345.79593741989925,"p99":389.4707619506445}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:58+02","p50":216.1525446666667,"p95":362.81613961797296,"p99":499.07007394086384}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:31:59+02","p50":224.3493111111111,"p95":356.99095574862196,"p99":454.9698276131661}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:00+02","p50":213.83111416666668,"p95":347.0250929388242,"p99":403.7637512958574}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:01+02","p50":222.49984299999997,"p95":339.04559366448564,"p99":396.24504114500377}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:02+02","p50":223.620714,"p95":335.90819822589737,"p99":410.91454987573576}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:03+02","p50":226.24937166666666,"p95":335.32358695026153,"p99":367.93180820174575}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:04+02","p50":218.08913655555557,"p95":435.8915802509346,"p99":489.28708586331175}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:05+02","p50":217.93237133333332,"p95":328.9693759232018,"p99":392.5351731268709}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:06+02","p50":222.4663835,"p95":406.6362453829376,"p99":500.8691196273427}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:07+02","p50":226.61218944444445,"p95":354.61878946562285,"p99":416.5205221994543}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:08+02","p50":222.77214600000002,"p95":374.46663888822206,"p99":450.8094589098823}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:09+02","p50":219.8126467777778,"p95":392.00014755658765,"p99":513.9537291904709}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:10+02","p50":224.03142144444442,"p95":352.97279644524684,"p99":435.4032528243065}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:11+02","p50":208.24536694444444,"p95":378.0713102377484,"p99":431.6813703477011}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:12+02","p50":209.8761542222222,"p95":369.5424052136536,"p99":451.2388182447891}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:13+02","p50":219.23786166666665,"p95":363.25913980668,"p99":451.12399298057295}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:14+02","p50":223.97180244444448,"p95":360.2883645230989,"p99":435.269975978215}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:15+02","p50":216.71007194444442,"p95":337.97084148830606,"p99":425.6973328247461}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:16+02","p50":233.9444117222222,"p95":425.433444166523,"p99":475.6684020970764}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:17+02","p50":231.26625750000002,"p95":434.71429186028485,"p99":520.2974088405571}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:18+02","p50":266.73170337500005,"p95":490.28090702285914,"p99":608.0986997067176}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:19+02","p50":324.23335299999997,"p95":515.6776790467342,"p99":609.0101567112661}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:20+02","p50":235.59441133333334,"p95":449.6935749005503,"p99":553.0957678274817}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:21+02","p50":218.5602545555556,"p95":372.14105988772064,"p99":430.6909131883452}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:22+02","p50":225.17245699999998,"p95":363.09618414827094,"p99":452.7774334426458}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:23+02","p50":210.15913133333333,"p95":373.7111297360679,"p99":461.5869807403679}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:24+02","p50":238.57727033333333,"p95":381.973308155411,"p99":477.66297981008245}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:25+02","p50":216.27082833333336,"p95":378.01305271718786,"p99":466.8391950271168}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:26+02","p50":242.8518598333333,"p95":359.6791281525676,"p99":428.9229971366134}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:27+02","p50":231.63725011111111,"p95":349.4298509186148,"p99":397.71019343576194}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:28+02","p50":226.732339,"p95":405.62277149829583,"p99":484.44164217784237}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:29+02","p50":228.32962716666668,"p95":361.53411524699186,"p99":414.4297816233115}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:30+02","p50":233.88495633333332,"p95":326.2271343763365,"p99":369.9302257789562}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:31+02","p50":226.84978122222222,"p95":360.97489976813193,"p99":463.8371062346909}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:32+02","p50":236.76296433333334,"p95":354.7024958877034,"p99":413.6180070585396}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:33+02","p50":224.9032802222222,"p95":375.59095100642446,"p99":419.86932278964713}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:34+02","p50":240.32421677777776,"p95":392.4488996499612,"p99":431.8389848001766}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:35+02","p50":230.42220444444447,"p95":378.56464276099706,"p99":505.89891194351765}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:36+02","p50":234.5692048888889,"p95":401.51542342983174,"p99":461.6675951622534}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:37+02","p50":227.309022,"p95":355.8756864912835,"p99":455.98563471131206}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:38+02","p50":224.007426,"p95":376.11202465679236,"p99":464.66399719672154}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:39+02","p50":228.86314066666668,"p95":356.3182932732327,"p99":403.7778434523003}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:40+02","p50":230.80453366666669,"p95":404.52124239434767,"p99":535.0275503524186}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:41+02","p50":218.62569861111112,"p95":360.09291733054164,"p99":439.7718299076786}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:42+02","p50":217.58812883333334,"p95":371.6389735550856,"p99":437.86308375483134}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:43+02","p50":233.06853166666664,"p95":399.19068332490605,"p99":479.7929705137174}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:44+02","p50":234.47938266666665,"p95":392.6643144576529,"p99":449.9907589968376}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:45+02","p50":232.08557983333333,"p95":382.68468427319885,"p99":488.2678124033661}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:46+02","p50":244.62408449999998,"p95":446.9696500811621,"p99":515.7216993169636}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:47+02","p50":255.19688,"p95":399.67054350503884,"p99":525.5660708170998}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:48+02","p50":285.83534387500004,"p95":487.26039093950254,"p99":544.4698768061297}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:49+02","p50":301.3556585,"p95":510.964604578166,"p99":635.2784504037246}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:50+02","p50":264.18729766666667,"p95":472.6264136536513,"p99":636.7521938510426}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:51+02","p50":227.35487550000002,"p95":377.1048645129325,"p99":458.53888587810513}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:52+02","p50":251.922576,"p95":447.350989489799,"p99":554.7686835216598}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:53+02","p50":263.13434474999997,"p95":530.8507723858329,"p99":721.0481155224113}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:54+02","p50":225.58388261111114,"p95":380.161137656028,"p99":468.34563998138475}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:55+02","p50":242.1133463333333,"p95":384.5402654924929,"p99":505.5946372716129}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:56+02","p50":232.08675422222223,"p95":384.213887055137,"p99":429.20222261100054}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:57+02","p50":223.09726055555555,"p95":404.80337730472684,"p99":511.3158119601204}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:58+02","p50":234.7839282777778,"p95":358.6693596630655,"p99":451.8204360131163}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:32:59+02","p50":232.87882766666667,"p95":425.7467114883852,"p99":487.68536225521467}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:00+02","p50":237.06443000000002,"p95":399.4266287533381,"p99":476.90989017555665}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:01+02","p50":248.4058461666667,"p95":396.54308326479105,"p99":474.22467991028736}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:02+02","p50":230.37859744444447,"p95":393.55309706906684,"p99":453.449538335906}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:03+02","p50":232.3341578333333,"p95":374.4735733048958,"p99":465.26900734739206}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:04+02","p50":264.482689875,"p95":410.27116344049404,"p99":469.7166974274752}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:05+02","p50":246.8850505,"p95":395.45494734583315,"p99":442.84180295915985}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:06+02","p50":230.89117733333333,"p95":353.4638644950161,"p99":389.47989380728535}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:07+02","p50":232.73668866666668,"p95":377.9326010976375,"p99":437.5844729626205}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:08+02","p50":235.79598088888892,"p95":416.665821860076,"p99":505.7929715148871}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:09+02","p50":233.68560366666665,"p95":373.1392250865016,"p99":418.8801051078911}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:10+02","p50":230.8417433333333,"p95":420.2855908858211,"p99":473.8649640798705}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:11+02","p50":223.24582850000002,"p95":393.8979325222867,"p99":476.9909705780468}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:12+02","p50":244.14954133333333,"p95":403.7375241260774,"p99":477.99835210016084}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:13+02","p50":246.41228644444445,"p95":361.8135735635612,"p99":402.8533168321066}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:14+02","p50":229.61298766666667,"p95":363.6033308717476,"p99":455.7843266998546}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:15+02","p50":244.89581355555552,"p95":400.5891745289134,"p99":456.673000659389}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:16+02","p50":253.74574712499998,"p95":398.5470624757168,"p99":436.57160895948317}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:17+02","p50":290.77609774999996,"p95":534.8039849119766,"p99":591.1491747143622}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:18+02","p50":287.630905,"p95":555.7053628811187,"p99":759.3057628708649}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:19+02","p50":260.139722,"p95":434.3915614445654,"p99":583.3201923909776}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:20+02","p50":243.41405466666666,"p95":369.17734439572047,"p99":455.30727302378466}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:21+02","p50":231.53368149999997,"p95":365.0432653110473,"p99":407.24337827021407}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:22+02","p50":242.505883,"p95":412.4999347425413,"p99":524.601658194849}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:23+02","p50":232.54534833333332,"p95":374.5556629026492,"p99":442.560841053041}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:24+02","p50":239.58688183333334,"p95":398.5046884272634,"p99":480.59723467273955}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:25+02","p50":247.6962111111111,"p95":387.1799167187638,"p99":464.32631229422833}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:26+02","p50":241.012872,"p95":368.4025947332088,"p99":447.3605011210189}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:27+02","p50":233.9766694444444,"p95":386.96228628693285,"p99":477.81864650684935}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:28+02","p50":235.542109,"p95":389.2316282820778,"p99":448.98848502568865}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:29+02","p50":245.02206544444445,"p95":393.23018982993966,"p99":459.86451168006994}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:30+02","p50":223.270794,"p95":355.406840181513,"p99":387.4610252394195}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:31+02","p50":239.80010344444443,"p95":394.09504108615874,"p99":494.7818513401794}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:32+02","p50":246.17028566666667,"p95":386.3242740246104,"p99":417.96801886121796}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:33+02","p50":247.03520083333333,"p95":390.1639436671072,"p99":459.0386687274175}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:34+02","p50":241.73285116666668,"p95":375.95783428324125,"p99":601.1601099873352}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:35+02","p50":255.778945125,"p95":419.4284088787367,"p99":545.8958604787023}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:36+02","p50":243.52722133333333,"p95":460.7516336206549,"p99":532.6451249426284}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:37+02","p50":244.04432766666665,"p95":388.6908727975695,"p99":422.97644730704116}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:38+02","p50":220.39450349999998,"p95":568.1284529653406,"p99":796.0735677623483}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:39+02","p50":246.928389,"p95":472.18584955523835,"p99":812.0410928928576}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:40+02","p50":227.95230333333333,"p95":370.18308553036735,"p99":465.310982619061}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:41+02","p50":227.30706838888887,"p95":399.8503266134108,"p99":454.9765421770229}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:42+02","p50":225.83264633333334,"p95":403.40213752225685,"p99":480.22409354110385}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:43+02","p50":243.92712533333335,"p95":377.5723761829224,"p99":432.4551040263641}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:44+02","p50":242.24754425,"p95":426.6340734641848,"p99":490.11104219123314}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:45+02","p50":257.8771213333333,"p95":407.0849706850665,"p99":560.2219665034747}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:46+02","p50":246.939657375,"p95":450.8215187908593,"p99":489.9558820302191}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:47+02","p50":290.98755325,"p95":497.5922189429208,"p99":551.4503608069765}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:48+02","p50":306.20941975,"p95":527.852640478988,"p99":641.807728294407}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:49+02","p50":292.03312174999996,"p95":518.6509610362588,"p99":583.629724640347}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:50+02","p50":252.55858233333333,"p95":437.9912643892018,"p99":561.7242892578134}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:51+02","p50":236.48447816666666,"p95":417.9918249314029,"p99":464.1891630546756}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:52+02","p50":248.784396,"p95":393.80324938535057,"p99":452.50154753660036}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:53+02","p50":239.0274007222222,"p95":373.2718030940702,"p99":433.1383861347618}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:54+02","p50":233.55698716666666,"p95":394.4068900888228,"p99":459.51528932271003}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:55+02","p50":248.1247286111111,"p95":422.1556352405434,"p99":461.31605151419063}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:56+02","p50":232.1230298888889,"p95":382.47190553403607,"p99":453.6062800068715}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:57+02","p50":245.4926118888889,"p95":439.4423141718396,"p99":481.5430297317166}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:58+02","p50":232.844899875,"p95":436.8947481744818,"p99":541.0581313336654}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:33:59+02","p50":266.1794130555556,"p95":384.41047095709104,"p99":434.79699793671415}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:00+02","p50":236.73512316666665,"p95":393.8240202940875,"p99":502.21442247265816}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:01+02","p50":231.73938011111113,"p95":375.5038520823298,"p99":490.72883558253477}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:02+02","p50":234.91059538888888,"p95":386.9306789033506,"p99":435.4778516190729}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:03+02","p50":250.90772750000002,"p95":418.5410255227275,"p99":484.13693815435596}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:04+02","p50":253.2914976666667,"p95":438.1993889250488,"p99":485.156488437088}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:05+02","p50":237.86275966666665,"p95":412.84416064722546,"p99":531.4925584635935}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:06+02","p50":241.4130301111111,"p95":384.6835320394778,"p99":466.4485572138905}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:07+02","p50":246.9962435,"p95":430.61991138997416,"p99":548.2566847912502}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:08+02","p50":252.04718633333334,"p95":415.8835463977967,"p99":506.81570539813833}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:09+02","p50":242.890341,"p95":415.68252231719333,"p99":459.3355598804331}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:10+02","p50":257.612865375,"p95":396.18150317915325,"p99":547.5985127782313}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:11+02","p50":241.42066133333336,"p95":384.0826483941463,"p99":475.19276299889685}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:12+02","p50":230.87129155555556,"p95":414.3070638313751,"p99":516.0454287644143}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:13+02","p50":257.038431375,"p95":395.0868904290263,"p99":520.8248982555652}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:14+02","p50":257.551038,"p95":392.02358257368945,"p99":446.8837309689145}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:15+02","p50":242.0045942222222,"p95":398.8031564911429,"p99":463.7348498225236}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:16+02","p50":263.97582162500004,"p95":470.7002272789613,"p99":596.6145870656634}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:17+02","p50":262.332346,"p95":475.3494265709779,"p99":658.0977595134699}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:18+02","p50":293.60957375,"p95":560.0902498358479,"p99":778.0352557814922}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:19+02","p50":269.52456137499996,"p95":487.2904865060484,"p99":606.1188282251981}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:20+02","p50":244.15401133333333,"p95":387.7079944607147,"p99":461.90501719173835}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:21+02","p50":242.50962461111112,"p95":407.23767418207547,"p99":450.81608595880124}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:22+02","p50":253.49955575,"p95":405.8076309284006,"p99":479.3382726868348}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:23+02","p50":274.9960685,"p95":428.6451952173658,"p99":535.5117178568265}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:24+02","p50":247.22204250000001,"p95":379.9838824606048,"p99":469.887887423934}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:25+02","p50":243.24941633333333,"p95":387.08692911201007,"p99":522.0354548741606}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:26+02","p50":256.83797849999996,"p95":435.508455010896,"p99":550.692052602832}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:27+02","p50":304.496019875,"p95":446.85923004462387,"p99":534.6948307420452}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:28+02","p50":257.481522125,"p95":445.0183366163345,"p99":538.8034372824739}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:29+02","p50":238.31051549999998,"p95":427.5492712721037,"p99":482.23812962444117}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:30+02","p50":247.265616125,"p95":450.8236542639635,"p99":507.2346711872134}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:31+02","p50":244.04404427777777,"p95":415.78895576956944,"p99":455.62288074609944}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:32+02","p50":244.34603562499998,"p95":433.7406606232805,"p99":522.8088149285817}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:33+02","p50":237.54384644444443,"p95":387.55320204594807,"p99":480.8667602889709}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:34+02","p50":258.3482255,"p95":397.8261588699484,"p99":489.93941125999265}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:35+02","p50":246.02377483333336,"p95":371.4415451996841,"p99":480.1354843534546}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:36+02","p50":246.23659855555556,"p95":398.50261767405425,"p99":550.2927599834298}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:37+02","p50":253.42273262499998,"p95":427.3329114535793,"p99":506.6480114395923}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:38+02","p50":246.60848294444443,"p95":395.13165166255476,"p99":462.32764865563485}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:39+02","p50":245.709733,"p95":422.92360313488217,"p99":558.3094882057648}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:40+02","p50":247.76838394444442,"p95":399.6298373327687,"p99":524.8320113544054}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:41+02","p50":231.99763283333334,"p95":396.4600884229193,"p99":468.1577781916847}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:42+02","p50":257.294788,"p95":412.975906559,"p99":477.2232233898587}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:43+02","p50":247.16260511111113,"p95":437.994821644352,"p99":616.0052474575958}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:44+02","p50":254.80792699999998,"p95":423.8831210833641,"p99":480.05263252854013}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:45+02","p50":250.00091700000002,"p95":444.52197677016926,"p99":514.3937235638236}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:46+02","p50":251.906231,"p95":532.076560612113,"p99":591.2071615058823}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:47+02","p50":262.25566625,"p95":464.3827456603379,"p99":609.9958891009597}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:48+02","p50":284.721244375,"p95":531.728444953583,"p99":640.5216567479637}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:49+02","p50":338.290922125,"p95":589.1906216921661,"p99":691.8721883108179}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:50+02","p50":320.7673855,"p95":515.5176785436153,"p99":632.0176443304506}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:51+02","p50":270.6670875,"p95":439.75460668313167,"p99":534.7910984565306}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:52+02","p50":247.18855844444445,"p95":414.9266299242001,"p99":449.263458878932}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:53+02","p50":252.72339575,"p95":461.86140911139967,"p99":506.16770534266567}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:54+02","p50":244.2179183333333,"p95":403.0196703677668,"p99":476.57731770493507}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:55+02","p50":254.95947675,"p95":428.0794374766185,"p99":480.5137230691939}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:56+02","p50":239.356133,"p95":417.97537687333147,"p99":575.2845023377519}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:57+02","p50":252.05772174999998,"p95":445.2623466346369,"p99":681.62964024632}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:58+02","p50":251.509722,"p95":406.68917443704447,"p99":488.2903484970379}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:34:59+02","p50":269.968730125,"p95":462.955597932823,"p99":517.51872375743}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:00+02","p50":271.291179625,"p95":408.15900895877274,"p99":468.0575583917348}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:01+02","p50":260.6519265,"p95":408.2398278800601,"p99":486.9506066071282}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:02+02","p50":249.08638399999998,"p95":402.8122317941038,"p99":425.2730846728821}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:03+02","p50":240.812377,"p95":387.9569571473711,"p99":487.22251631128}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:04+02","p50":255.25976799999998,"p95":389.6793000755879,"p99":468.7659897720661}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:05+02","p50":250.62307099999998,"p95":410.2871182714511,"p99":497.14616014005856}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:06+02","p50":246.606369,"p95":439.569528999143,"p99":540.7300664410473}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:07+02","p50":250.63701862499997,"p95":439.88441669453726,"p99":495.77547170241456}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:08+02","p50":250.99789633333333,"p95":384.7336377220992,"p99":460.1935018582964}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:09+02","p50":255.797218625,"p95":382.21282533441325,"p99":437.8775799206567}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:10+02","p50":260.1340413333333,"p95":462.7808593478576,"p99":544.0607919301765}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:11+02","p50":255.126604,"p95":437.71638282977256,"p99":509.5122343222742}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:12+02","p50":254.16688375,"p95":469.7708445514171,"p99":596.4816492280378}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:13+02","p50":255.737143,"p95":470.07591599670377,"p99":555.2960708614478}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:14+02","p50":251.8899373333333,"p95":401.569131154243,"p99":439.75819112880185}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:15+02","p50":252.79897012500004,"p95":382.81354998726715,"p99":466.82819427431633}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:16+02","p50":258.368427625,"p95":478.7574591130687,"p99":550.6379207093775}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:17+02","p50":269.95823225000004,"p95":508.84886587641716,"p99":581.3479067453766}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:18+02","p50":319.929041875,"p95":623.0270191611027,"p99":744.8653670339498}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:19+02","p50":320.10733,"p95":578.515384161166,"p99":802.1934291480673}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:20+02","p50":254.131393125,"p95":429.48825539788913,"p99":520.7591032995374}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:21+02","p50":263.7090465,"p95":464.01810903141245,"p99":543.333945996479}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:22+02","p50":265.382925,"p95":444.47017766590363,"p99":499.2965828000684}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:23+02","p50":262.59565216666664,"p95":445.50939130510335,"p99":505.75773296498255}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:24+02","p50":255.2947625,"p95":447.27120964095207,"p99":522.845152996975}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:25+02","p50":266.405042,"p95":430.21109693399,"p99":527.1653022898302}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:26+02","p50":257.5719805,"p95":425.71357152069425,"p99":475.99853154133797}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:27+02","p50":258.42057625,"p95":451.2807406978629,"p99":519.8596373823572}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:28+02","p50":263.750858875,"p95":413.1412213547309,"p99":501.4988705335603}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:29+02","p50":261.3700325,"p95":459.44244356627036,"p99":545.8286856570244}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:30+02","p50":259.16923124999994,"p95":394.999657350305,"p99":474.09946806015637}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:31+02","p50":260.73462974999995,"p95":402.0080005847919,"p99":480.7240314587393}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:32+02","p50":247.95554925,"p95":443.7761597778764,"p99":568.4041524165335}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:33+02","p50":273.68144574999997,"p95":418.8592395719484,"p99":445.7310229781904}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:34+02","p50":271.24839637499997,"p95":415.6710178238098,"p99":474.1082294845698}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:35+02","p50":271.00155233333334,"p95":464.5032853302579,"p99":564.5255068388929}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:36+02","p50":270.057148,"p95":429.41141660477575,"p99":489.42654765290973}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:37+02","p50":279.260726875,"p95":473.10020290691523,"p99":517.9944379044347}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:38+02","p50":259.893681,"p95":475.40265398043715,"p99":516.7147075678382}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:39+02","p50":241.16473766666664,"p95":401.106435010652,"p99":486.8981021822858}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:40+02","p50":264.733921,"p95":438.0484842146253,"p99":464.77467181394763}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:41+02","p50":253.783912,"p95":390.63252882195974,"p99":489.4342463915157}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:42+02","p50":248.65580075000003,"p95":394.7830699308772,"p99":504.30505801576044}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:43+02","p50":295.8947515,"p95":528.4741479112611,"p99":617.1819563232746}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:44+02","p50":284.473048125,"p95":454.27704807440466,"p99":527.7279918741376}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:45+02","p50":249.64267566666663,"p95":436.2224412011648,"p99":497.9041390465624}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:46+02","p50":270.368884,"p95":517.5866726913666,"p99":571.0812230935578}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:47+02","p50":314.154541625,"p95":554.5483804525378,"p99":697.1422834208388}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:48+02","p50":330.89126825,"p95":566.703627649873,"p99":684.9721087983279}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:49+02","p50":327.57335762499997,"p95":602.7722687312624,"p99":716.9294886656778}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:50+02","p50":267.41302675,"p95":488.1631517109785,"p99":536.3552451085968}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:51+02","p50":268.711671625,"p95":393.6989624800772,"p99":445.9623439904365}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:52+02","p50":258.78581877777776,"p95":406.33913889978095,"p99":484.40464599093536}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:53+02","p50":255.63745699999998,"p95":416.6679169158888,"p99":486.7021321201973}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:54+02","p50":253.28816762499997,"p95":424.1734274351335,"p99":505.7028296532907}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:55+02","p50":261.75738533333333,"p95":439.2747612949972,"p99":470.0899060178227}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:56+02","p50":254.1992995,"p95":438.37555251568114,"p99":504.4832310588999}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:57+02","p50":271.47276312500003,"p95":398.8732578309153,"p99":530.2756969538971}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:58+02","p50":265.66792,"p95":443.20559678939367,"p99":533.555235527616}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:35:59+02","p50":266.24755,"p95":441.10460870551316,"p99":464.42411755782365}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:00+02","p50":247.36104799999998,"p95":424.21022066528747,"p99":525.1935427021211}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:01+02","p50":308.055564,"p95":547.7484162400189,"p99":754.7858044313889}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:02+02","p50":297.69547950000003,"p95":470.56979023146846,"p99":550.9621809520797}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:03+02","p50":273.73311424999997,"p95":391.54654427341166,"p99":467.90297496202754}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:04+02","p50":279.3649785,"p95":440.68600435456733,"p99":601.7719794049216}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:05+02","p50":260.66567775,"p95":415.4385318268583,"p99":498.1817095879083}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:06+02","p50":244.37345525,"p95":419.7056218058458,"p99":501.14800422493624}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:07+02","p50":265.3689635,"p95":454.1364713933226,"p99":499.5201940366664}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:08+02","p50":282.50187800000003,"p95":476.86135509599245,"p99":544.7850116231317}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:09+02","p50":258.74034725,"p95":421.0682923532711,"p99":549.8008959157944}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:10+02","p50":250.69572349999999,"p95":479.76608016887957,"p99":563.4040498106232}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:11+02","p50":265.02781425,"p95":418.6808485449755,"p99":479.3354043331985}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:12+02","p50":287.13989349999997,"p95":417.95321612878786,"p99":493.66395250505826}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:13+02","p50":265.90458675,"p95":429.3374977181099,"p99":540.6860666412163}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:14+02","p50":267.35199725,"p95":462.12737333626444,"p99":518.3628795830326}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:15+02","p50":270.26036425,"p95":425.64670490455245,"p99":492.6650219917927}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:16+02","p50":267.952325,"p95":457.5532252522965,"p99":663.9122299074402}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:17+02","p50":274.56824300000005,"p95":495.6762548673733,"p99":628.0609482288104}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:18+02","p50":342.886177375,"p95":563.050607191615,"p99":773.9684411976912}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:19+02","p50":311.35315875000003,"p95":618.8045532641,"p99":676.2139193190757}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:20+02","p50":258.68767425,"p95":434.68928235208654,"p99":527.718808758278}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:21+02","p50":264.1264005,"p95":432.4782100336876,"p99":496.36295273950196}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:22+02","p50":284.1780665,"p95":443.2347498551246,"p99":518.5824665314648}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:23+02","p50":281.483592875,"p95":419.242792908064,"p99":490.7284401088722}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:24+02","p50":272.64804025,"p95":416.24347988170456,"p99":472.5741446776733}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:25+02","p50":280.950089125,"p95":426.0589155050207,"p99":566.034674371351}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:26+02","p50":251.6446375,"p95":396.3501515190627,"p99":495.75921183264967}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:27+02","p50":271.6921563333333,"p95":474.90097030203987,"p99":519.697609997715}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:28+02","p50":266.936282875,"p95":442.30553009358806,"p99":509.80180702896024}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:29+02","p50":260.72009649999995,"p95":455.6411531602584,"p99":524.1748620250402}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:30+02","p50":264.44158949999996,"p95":404.5745870633924,"p99":491.91618528585434}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:31+02","p50":269.81570650000003,"p95":419.10434405671907,"p99":488.8180724874473}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:32+02","p50":260.94705250000004,"p95":454.2397197066002,"p99":510.50326549041364}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:33+02","p50":274.13322,"p95":439.01972856016585,"p99":482.5345382981339}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:34+02","p50":254.45976655555557,"p95":408.23470826867293,"p99":513.4013226754913}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:35+02","p50":267.26025725,"p95":464.78726307099674,"p99":540.4649847713737}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:36+02","p50":263.451475,"p95":439.8489624030719,"p99":528.4659436606503}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:37+02","p50":273.681362,"p95":413.1530599547871,"p99":475.3887482464139}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:38+02","p50":262.42256325,"p95":455.4396345786124,"p99":580.7996150937603}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:39+02","p50":257.9202376666667,"p95":437.1068716493566,"p99":486.99432259580396}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:40+02","p50":258.56056875,"p95":433.37247478405715,"p99":555.8528159423342}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:41+02","p50":264.389997,"p95":438.2981008643098,"p99":547.3491188609486}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:42+02","p50":276.49165312499997,"p95":430.88875801244956,"p99":485.78888851100425}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:43+02","p50":280.752932125,"p95":446.3170856669779,"p99":531.9637418814247}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:44+02","p50":282.32115875,"p95":484.47938888746734,"p99":602.8593631017479}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:45+02","p50":263.9226543333333,"p95":445.83545400655476,"p99":601.6209245441456}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:46+02","p50":291.68244425,"p95":497.7035354248284,"p99":679.3175245265692}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:47+02","p50":312.990089125,"p95":486.8317967950743,"p99":505.312604825201}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:48+02","p50":329.2914065,"p95":551.4049445607922,"p99":680.2606640784159}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:49+02","p50":339.16039,"p95":593.5391209677692,"p99":646.3333434984534}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:50+02","p50":276.62490599999995,"p95":558.3999952588991,"p99":600.6332137365399}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:51+02","p50":250.670752,"p95":409.05569041803824,"p99":533.5402634348641}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:52+02","p50":272.426645375,"p95":571.8618228908813,"p99":675.4606616498987}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:53+02","p50":267.51233625,"p95":422.6446393228599,"p99":531.6386685849448}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:54+02","p50":291.44980050000004,"p95":454.23875224000966,"p99":493.3651159520278}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:55+02","p50":274.67588487499995,"p95":441.77680315136587,"p99":540.8149981984445}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:56+02","p50":267.484300125,"p95":458.66542662605326,"p99":599.1730176369266}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:57+02","p50":262.621663,"p95":493.3482629882154,"p99":551.2015649434089}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:58+02","p50":281.55920000000003,"p95":445.7603713362245,"p99":496.409015278841}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:36:59+02","p50":278.549184,"p95":460.764101054255,"p99":532.8517307298717}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:00+02","p50":257.17045125,"p95":455.6271916242397,"p99":546.5039346143418}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:01+02","p50":271.48472825,"p95":427.6455074257917,"p99":533.3504298355958}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:02+02","p50":277.759975125,"p95":447.2331365456114,"p99":500.83704259983995}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:03+02","p50":275.49270187499997,"p95":405.2120361834234,"p99":479.98993033915207}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:04+02","p50":281.118100625,"p95":510.59994351032475,"p99":613.3954287719366}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:05+02","p50":271.03451125000004,"p95":475.3531457091773,"p99":520.8336449481296}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:06+02","p50":263.57687075,"p95":455.16643932775304,"p99":563.6589416264572}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:07+02","p50":290.43707325,"p95":472.7174846760864,"p99":560.3053150923405}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:08+02","p50":286.53179824999995,"p95":453.03868992775585,"p99":570.84568331039}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:09+02","p50":272.77136099999996,"p95":423.93573187629914,"p99":505.7369940903995}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:10+02","p50":280.7256185,"p95":463.9208990961666,"p99":520.664117339571}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:11+02","p50":275.613451,"p95":467.25170113098903,"p99":517.4252537377548}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:12+02","p50":248.20316125,"p95":454.2489062066117,"p99":547.0151625803135}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:13+02","p50":271.80180625,"p95":494.06151770305723,"p99":552.4011694815388}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:14+02","p50":290.31772524999997,"p95":521.2814904891643,"p99":599.5541574068633}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:15+02","p50":278.0536915,"p95":448.11469075231,"p99":561.162434063208}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:16+02","p50":313.3462895,"p95":619.5034486344389,"p99":702.6555097997456}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:17+02","p50":324.854590625,"p95":579.6841767499525,"p99":750.3554944991853}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:18+02","p50":343.512835,"p95":627.1252977442357,"p99":678.921302243248}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:19+02","p50":347.22449875,"p95":641.9308238686408,"p99":726.5546061571674}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:20+02","p50":272.753773,"p95":456.93943756108297,"p99":545.0433867799397}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:21+02","p50":287.305234,"p95":482.93960357461015,"p99":527.3300204129798}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:22+02","p50":291.84331899999995,"p95":459.0205791523464,"p99":636.5230674784857}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:23+02","p50":272.12157075000005,"p95":421.2066189373023,"p99":493.5883063361807}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:24+02","p50":275.994239875,"p95":472.16574953047,"p99":525.6466085327122}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:25+02","p50":282.369423,"p95":400.93488896156776,"p99":504.1080421592221}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:26+02","p50":279.14357125000004,"p95":435.26581782687947,"p99":490.03172875637534}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:27+02","p50":278.045139,"p95":437.25978724796465,"p99":525.4959172818178}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:28+02","p50":280.65772525,"p95":491.2533090767055,"p99":595.1768315263453}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:29+02","p50":294.69866025,"p95":483.799874165232,"p99":563.3968928920141}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:30+02","p50":280.822649,"p95":428.03382758572747,"p99":464.46219796906854}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:31+02","p50":275.64132825,"p95":446.15891757751916,"p99":507.87054447163007}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:32+02","p50":269.5587385,"p95":430.96664200211677,"p99":669.9642561105194}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:33+02","p50":279.663449125,"p95":438.6697980329419,"p99":520.7928192715287}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:34+02","p50":289.255497,"p95":499.0736842099783,"p99":528.0643940131398}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:35+02","p50":322.683688,"p95":558.1222331505617,"p99":758.9454577473765}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:36+02","p50":295.59765225,"p95":624.6576934951707,"p99":714.7788967299953}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:37+02","p50":261.1673405,"p95":435.0841642586198,"p99":480.58191710951616}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:38+02","p50":276.519091,"p95":478.10282919305,"p99":546.4376535161714}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:39+02","p50":267.70598937499994,"p95":442.08557746862556,"p99":526.6979442846423}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:40+02","p50":278.05879112499997,"p95":459.12646396856746,"p99":562.7794775632623}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:41+02","p50":268.1261875,"p95":480.67277178579377,"p99":526.5582824514285}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:42+02","p50":265.645195625,"p95":445.7156446246986,"p99":535.8363589735332}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:43+02","p50":281.2477945,"p95":449.5835557982304,"p99":515.9120453508282}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:44+02","p50":275.003384375,"p95":514.9206479877232,"p99":594.6198214315237}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:45+02","p50":270.0799885,"p95":454.97443280930185,"p99":524.0589443352184}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:46+02","p50":295.65093625000003,"p95":492.2391704338405,"p99":534.6472352812043}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:47+02","p50":274.08601450000003,"p95":442.37287061170724,"p99":586.8032892054558}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:48+02","p50":374.62705025,"p95":614.2161898959117,"p99":718.6417095456095}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:49+02","p50":384.14167225,"p95":646.9470666795946,"p99":709.5936829303309}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:50+02","p50":307.04816675,"p95":580.9987341745286,"p99":657.1635294630051}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:51+02","p50":264.51252750000003,"p95":405.9214381572137,"p99":485.2863852560253}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:52+02","p50":288.733014625,"p95":436.28569338081195,"p99":499.4273233914039}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:53+02","p50":292.59008775,"p95":444.8126474846289,"p99":559.6404062197221}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:54+02","p50":265.45254250000005,"p95":444.6517892553885,"p99":521.8394724585886}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:55+02","p50":276.110035,"p95":439.42550232806536,"p99":510.11889809915357}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:56+02","p50":269.87857225,"p95":408.3670248879873,"p99":486.81305099183794}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:57+02","p50":274.41081124999994,"p95":439.4784172497368,"p99":487.9560559297927}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:58+02","p50":272.0299305,"p95":483.57158045087084,"p99":564.1303695331484}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:37:59+02","p50":294.5038185,"p95":440.11316648074296,"p99":542.4094273271429}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:00+02","p50":294.01674149999997,"p95":473.3424680057596,"p99":583.2569641643033}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:01+02","p50":295.58635225,"p95":455.02840676387115,"p99":521.1404888461552}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:02+02","p50":303.802941125,"p95":451.2165169351802,"p99":511.0916807555754}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:03+02","p50":286.47689275000005,"p95":479.99778429102633,"p99":567.6860556975946}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:04+02","p50":283.83911750000004,"p95":466.3207577074134,"p99":560.1058221574355}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:05+02","p50":296.90665624999997,"p95":433.7798157127762,"p99":575.7135354705439}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:06+02","p50":270.504869,"p95":483.0571026430154,"p99":556.9352219606143}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:07+02","p50":272.069074625,"p95":457.8241872004946,"p99":521.6077715802451}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:08+02","p50":286.7748935,"p95":514.7952817698724,"p99":680.5006881987887}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:09+02","p50":279.39346625,"p95":460.0734857394414,"p99":528.4559263084283}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:10+02","p50":292.86708899999996,"p95":448.0299446216389,"p99":584.2344832331862}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:11+02","p50":302.73789650000003,"p95":513.5038546738189,"p99":615.2702772098422}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:12+02","p50":278.865654875,"p95":441.64487278980135,"p99":511.978653663738}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:13+02","p50":284.784116125,"p95":457.41361447205577,"p99":533.5180968186232}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:14+02","p50":275.32788000000005,"p95":458.3162684359131,"p99":556.7599309027099}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:15+02","p50":280.3231195,"p95":445.0551637334065,"p99":582.3993081457844}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:16+02","p50":301.43420525,"p95":530.2744790843103,"p99":640.1278627152329}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:17+02","p50":364.4796525,"p95":578.1403585420021,"p99":603.9809204001413}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:18+02","p50":372.29258325,"p95":704.1429628307343,"p99":774.5320789924278}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:19+02","p50":360.78676,"p95":610.7048109200895,"p99":736.5407505721814}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:20+02","p50":314.049614,"p95":613.2237474310322,"p99":762.3890784030613}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:21+02","p50":282.7069275,"p95":475.41358644634107,"p99":535.6855813607083}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:22+02","p50":278.298527,"p95":424.1571578855939,"p99":457.30417616728636}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:23+02","p50":284.983961125,"p95":454.2553327711692,"p99":562.3987605130988}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:24+02","p50":323.141547375,"p95":521.9283128823034,"p99":546.8692202282961}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:25+02","p50":304.54330775,"p95":471.48780923213565,"p99":526.0347048071785}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:26+02","p50":292.379567,"p95":471.84134074121897,"p99":520.0252552257032}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:27+02","p50":299.916064,"p95":462.18045082281066,"p99":526.3080132471199}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:28+02","p50":294.7068685,"p95":454.14565936854245,"p99":562.630079958622}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:29+02","p50":282.64017725,"p95":462.15185026630354,"p99":524.8979255207462}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:30+02","p50":298.0146975,"p95":453.1234867757131,"p99":501.8353833705611}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:31+02","p50":285.517019375,"p95":474.32733556678096,"p99":607.0127939875014}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:32+02","p50":316.60584700000004,"p95":436.0858531417457,"p99":564.3585470786509}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:33+02","p50":276.259842625,"p95":432.824868558483,"p99":535.3429050789955}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:34+02","p50":288.039445875,"p95":457.41460762805696,"p99":526.8870811959748}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:35+02","p50":304.45541275,"p95":433.01749780563546,"p99":529.5591907882152}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:36+02","p50":290.926061375,"p95":469.41253246797226,"p99":567.7128335475919}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:37+02","p50":289.969403,"p95":460.42590394879903,"p99":514.1239983952655}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:38+02","p50":291.094792375,"p95":470.95182639752295,"p99":565.8061726239405}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:39+02","p50":293.434761,"p95":447.9659659214381,"p99":530.5451185746626}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:40+02","p50":282.9847665,"p95":504.0198940813208,"p99":627.297626658765}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:41+02","p50":281.394672,"p95":508.38208074886796,"p99":705.631788158936}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:42+02","p50":283.62036,"p95":456.1519186505919,"p99":527.400940347641}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:43+02","p50":296.907472,"p95":470.9682533727894,"p99":577.844560855461}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:44+02","p50":309.17395650000003,"p95":489.76920722176743,"p99":538.4678099386673}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:45+02","p50":284.83044674999996,"p95":511.9471709640339,"p99":638.2307501275484}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:46+02","p50":324.24727399999995,"p95":514.4866818407793,"p99":653.1810312161302}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:47+02","p50":292.47771575,"p95":460.39423205013384,"p99":495.5933168066232}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:48+02","p50":361.477539,"p95":702.9946877955523,"p99":863.3035797861188}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:49+02","p50":347.163134,"p95":722.0058020472488,"p99":865.4673349531555}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:50+02","p50":330.57022037499996,"p95":703.469716732043,"p99":835.1083508007011}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:51+02","p50":284.812362125,"p95":496.3238719336624,"p99":557.5309844423194}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:52+02","p50":299.3001195,"p95":471.77067894943366,"p99":533.9160366149049}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:53+02","p50":292.06249325,"p95":457.5616723272429,"p99":534.5011652540618}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:54+02","p50":294.1137175,"p95":482.43980061224505,"p99":584.8751213080201}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:55+02","p50":305.75706875,"p95":467.83652219410607,"p99":503.4050448995853}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:56+02","p50":283.83761525,"p95":447.8575097564939,"p99":507.14210297411324}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:57+02","p50":282.3411345,"p95":459.26619966412596,"p99":536.5603360367336}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:58+02","p50":295.537558625,"p95":458.08335727020454,"p99":522.9812536097942}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:38:59+02","p50":294.9872435,"p95":437.5866677396869,"p99":520.258768646837}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:00+02","p50":303.54293925,"p95":446.6625401886058,"p99":533.2071036481738}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:01+02","p50":289.70852399999995,"p95":507.7120067326195,"p99":537.6489259911264}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:02+02","p50":297.58836175,"p95":448.46979712468783,"p99":483.5936854827015}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:03+02","p50":286.61284487499995,"p95":467.224118861816,"p99":645.2514354724825}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:04+02","p50":296.334842125,"p95":473.1896239246984,"p99":604.3109553382325}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:05+02","p50":282.262784625,"p95":500.17499748450706,"p99":592.0214839451652}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:06+02","p50":299.263343125,"p95":450.785239057786,"p99":504.1242538318785}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:07+02","p50":306.72614887500004,"p95":503.68155193797935,"p99":639.3609644677372}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:08+02","p50":388.798586375,"p95":595.2196786744255,"p99":670.3795597631452}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:09+02","p50":313.7536325,"p95":499.0897153087473,"p99":622.1571526460896}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:10+02","p50":292.8736505,"p95":475.12504706742715,"p99":615.6898882612362}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:11+02","p50":287.72190224999997,"p95":470.03727405909814,"p99":521.5958924355562}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:12+02","p50":294.019459,"p95":421.21563264722636,"p99":481.86109114819715}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:13+02","p50":291.92281475,"p95":495.63964192564924,"p99":578.7918773636441}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:14+02","p50":278.930134125,"p95":523.824413685155,"p99":609.3752381564026}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:15+02","p50":279.509395625,"p95":470.93278338554813,"p99":641.5617679089451}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:16+02","p50":335.45656575,"p95":523.4278485714962,"p99":637.7313868931882}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:17+02","p50":313.2136555,"p95":500.6458104606156,"p99":643.7488418568447}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:18+02","p50":411.448944,"p95":659.0515835216574,"p99":692.7601942719373}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:19+02","p50":323.019279,"p95":633.9363027837603,"p99":831.3469673552698}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:20+02","p50":291.513243,"p95":434.4102281724081,"p99":498.8238963223419}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:21+02","p50":278.19095050000004,"p95":499.5846070849953,"p99":589.5221163984547}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:22+02","p50":275.92579249999994,"p95":478.9669818728042,"p99":539.2421971128979}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:23+02","p50":281.12221275,"p95":489.3655113104573,"p99":532.2930449638558}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:24+02","p50":287.6263035,"p95":473.5327284865749,"p99":527.5643380773906}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:25+02","p50":285.844445,"p95":476.5885602038138,"p99":596.8086480897759}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:26+02","p50":276.659429,"p95":483.23488431402643,"p99":553.1055383279042}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:27+02","p50":292.8026555,"p95":437.6286556834668,"p99":562.5607714173016}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:28+02","p50":289.2635385,"p95":468.5254924632532,"p99":495.9771608658907}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:29+02","p50":289.365346125,"p95":486.97665285133246,"p99":566.1052013866753}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:30+02","p50":269.47354875,"p95":444.0457907483142,"p99":522.7294281529155}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:31+02","p50":316.68122225,"p95":492.2670159791336,"p99":599.7145179515344}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:32+02","p50":278.94372112499997,"p95":494.4792191545054,"p99":543.801619090188}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:33+02","p50":290.986736875,"p95":449.1992817243483,"p99":516.4929860083512}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:34+02","p50":301.91486,"p95":445.82400281291024,"p99":522.3193553207207}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:35+02","p50":299.5034515,"p95":476.16217412559655,"p99":523.689367272183}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:36+02","p50":308.474279625,"p95":499.1238591518811,"p99":589.2124832457032}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:37+02","p50":288.27218625,"p95":442.9043426567468,"p99":485.648556105432}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:38+02","p50":302.109373,"p95":486.71924880357255,"p99":530.5639405975213}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:39+02","p50":282.0861065,"p95":455.0125066332538,"p99":529.4192212293186}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:40+02","p50":299.5464095,"p95":492.77070150722494,"p99":573.5600539197319}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:41+02","p50":292.971954,"p95":484.06476146641495,"p99":570.5580891688219}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:42+02","p50":311.76950550000004,"p95":439.17795159897804,"p99":511.91554700526433}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:43+02","p50":297.67031762500005,"p95":458.6554031959731,"p99":538.8457759646666}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:44+02","p50":296.6030455,"p95":496.20232048572876,"p99":567.816771563633}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:45+02","p50":310.283842875,"p95":463.5535547799375,"p99":530.6328237687378}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:46+02","p50":341.0196415,"p95":526.4154583080785,"p99":612.0404280131067}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:47+02","p50":309.946251625,"p95":471.0679451358792,"p99":506.83550367945577}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:48+02","p50":408.892771,"p95":612.1856057077842,"p99":753.6998635432913}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:49+02","p50":375.8138045,"p95":658.1641567728309,"p99":878.4842786047897}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:50+02","p50":324.0173475,"p95":670.7768270410317,"p99":862.9408644143576}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:51+02","p50":297.91106625,"p95":495.0000450699778,"p99":574.8004294732361}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:52+02","p50":305.74394675,"p95":439.0423736705117,"p99":519.7990007475805}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:53+02","p50":312.20286575,"p95":449.5555775160666,"p99":504.76247497174694}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:54+02","p50":294.8860905,"p95":501.68926737833505,"p99":564.6064657294828}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:55+02","p50":310.47490425,"p95":492.3080736637569,"p99":545.570634742797}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:56+02","p50":300.711862,"p95":448.523817298154,"p99":547.4589189224587}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:57+02","p50":285.361698875,"p95":426.74876815992815,"p99":544.319046544888}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:58+02","p50":284.739319,"p95":455.2454605485891,"p99":505.4754467727804}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:39:59+02","p50":303.65188,"p95":428.5325339329714,"p99":492.497304639669}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:00+02","p50":313.89264749999995,"p95":465.8342206242869,"p99":525.137867824834}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:01+02","p50":305.00714975,"p95":486.31293282384075,"p99":553.5618117954156}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:02+02","p50":302.10090625,"p95":441.6590150125473,"p99":504.86669759222843}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:03+02","p50":314.53987037499996,"p95":522.940527460797,"p99":617.7475047280867}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:04+02","p50":327.82806875,"p95":459.01161000943864,"p99":554.916915678565}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:05+02","p50":295.581257625,"p95":509.83931894021174,"p99":571.2683574212518}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:06+02","p50":298.40544324999996,"p95":446.2547257418549,"p99":511.4758402779052}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:07+02","p50":288.66548800000004,"p95":471.302635162027,"p99":517.955740443758}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:08+02","p50":306.576096,"p95":462.53459915288596,"p99":579.3859896210632}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:09+02","p50":303.828008,"p95":488.7604992657844,"p99":614.1007459973416}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:10+02","p50":297.930899875,"p95":437.89464585127587,"p99":598.5329666812044}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:11+02","p50":296.85393650000003,"p95":460.11099566975935,"p99":553.600011615623}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:12+02","p50":292.716156125,"p95":487.6084974831651,"p99":533.3088350333521}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:13+02","p50":309.138551,"p95":521.042852219364,"p99":577.1700352138172}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:14+02","p50":323.65315875,"p95":458.5195512760284,"p99":495.41461625261974}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:15+02","p50":299.84042124999996,"p95":474.80214611007483,"p99":560.7688814104268}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:16+02","p50":323.49443275,"p95":526.8861012911833,"p99":609.8603744477457}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:17+02","p50":353.7937945,"p95":606.0710592379974,"p99":737.9176218182773}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:18+02","p50":399.14815600000003,"p95":635.1591814926941,"p99":802.3005647703493}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:19+02","p50":356.54469274999997,"p95":634.793854917179,"p99":699.0339292064401}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:20+02","p50":297.6499905,"p95":486.16026861651227,"p99":669.111151363678}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:21+02","p50":300.78904025,"p95":472.0395010926522,"p99":587.9472115621968}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:22+02","p50":299.28269774999995,"p95":535.6570898374453,"p99":647.6625711216443}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:23+02","p50":306.28313349999996,"p95":491.04700728000256,"p99":572.0145518629989}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:24+02","p50":297.6418995,"p95":444.35196201995467,"p99":560.9877170154724}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:25+02","p50":306.4591745,"p95":448.0956320624989,"p99":503.77743094976614}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:26+02","p50":307.7786835,"p95":502.2503078251931,"p99":584.5106716337091}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:27+02","p50":307.887486875,"p95":489.97355275501536,"p99":561.8933482998109}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:28+02","p50":296.552464,"p95":492.39513455147284,"p99":575.1306444471833}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:29+02","p50":304.83733962499997,"p95":469.97202904058065,"p99":540.6657381220723}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:30+02","p50":297.06478787500004,"p95":508.65188531755604,"p99":660.0527787129575}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:31+02","p50":332.04694025000003,"p95":486.4280021698817,"p99":596.3989477609406}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:32+02","p50":294.55854,"p95":488.3112866523909,"p99":522.8180566459126}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:33+02","p50":297.38822175,"p95":530.4617622704887,"p99":577.5223368259461}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:34+02","p50":307.84212249999996,"p95":478.93599936241674,"p99":492.83389687346266}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:35+02","p50":303.214311375,"p95":500.39238821960964,"p99":573.1897440080343}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:36+02","p50":308.248709625,"p95":512.7063705251439,"p99":549.0270344249051}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:37+02","p50":319.3844325,"p95":454.44361620906085,"p99":479.01550769645667}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:38+02","p50":309.10320075,"p95":460.1251080360451,"p99":538.551745776207}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:39+02","p50":309.55519400000003,"p95":524.1800892570419,"p99":595.7085964175634}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:40+02","p50":308.913221125,"p95":482.71049663483933,"p99":512.1885086505567}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:41+02","p50":304.82266549999997,"p95":523.9982732200986,"p99":658.6942209644012}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:42+02","p50":309.01759749999997,"p95":482.5976166110712,"p99":539.3997563117584}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:43+02","p50":306.00587162499994,"p95":503.6285551948016,"p99":590.7161858025829}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:44+02","p50":373.21234125,"p95":607.9167637566718,"p99":724.1480606345654}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:45+02","p50":315.50000550000004,"p95":580.1693204951665,"p99":664.2680299818707}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:46+02","p50":345.46472274999996,"p95":572.9140044497159,"p99":629.8515626681366}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:47+02","p50":335.88026549999995,"p95":645.0842873623425,"p99":818.6681102975144}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:48+02","p50":356.88882087499996,"p95":596.6027891325829,"p99":768.5156516738011}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:49+02","p50":380.8114975,"p95":560.6301455998201,"p99":623.0124684732971}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:50+02","p50":391.33717,"p95":709.434419001908,"p99":791.0037296776726}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:51+02","p50":300.99483925000004,"p95":548.6453005997798,"p99":809.444665581554}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:52+02","p50":311.69538,"p95":529.7192219903793,"p99":655.2737100052185}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:53+02","p50":321.52313200000003,"p95":519.3159208392374,"p99":664.8758497415256}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:54+02","p50":297.32266925,"p95":513.4077142465279,"p99":594.4405908971854}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:55+02","p50":299.3202725,"p95":474.77076873040846,"p99":558.6933262662697}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:56+02","p50":316.05039324999996,"p95":507.46733855047296,"p99":651.8103151627821}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:57+02","p50":291.718621875,"p95":484.4998487995039,"p99":567.7552356438165}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:58+02","p50":305.58951912500004,"p95":524.6972196583592,"p99":584.3777284589825}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:40:59+02","p50":312.85441375000005,"p95":455.7878425775516,"p99":557.08559745153}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:00+02","p50":313.15196349999997,"p95":526.8877192496118,"p99":620.5821026369466}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:01+02","p50":329.83175575,"p95":512.0655578011895,"p99":615.1726061007461}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:02+02","p50":308.7025895,"p95":486.3438130353107,"p99":553.8112277463046}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:03+02","p50":311.64479700000004,"p95":505.6599191714747,"p99":638.9092862032937}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:04+02","p50":344.98547987499995,"p95":524.8518197825521,"p99":625.855102418458}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:05+02","p50":338.65577825,"p95":511.973442047192,"p99":619.0625848790436}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:06+02","p50":327.2365685,"p95":532.8625153432456,"p99":624.5471610542664}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:07+02","p50":348.76077075,"p95":551.1159969202657,"p99":711.7168278118286}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:08+02","p50":323.111841,"p95":469.02636642978285,"p99":531.8206015098572}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:09+02","p50":304.785499125,"p95":466.42241931136516,"p99":538.9242017375652}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:10+02","p50":283.58626275,"p95":555.9782627755885,"p99":685.2704095387935}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:11+02","p50":295.619051,"p95":472.8689771328274,"p99":590.1278786215696}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:12+02","p50":298.2463655,"p95":493.30942877872025,"p99":594.2155398430829}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:13+02","p50":297.318209375,"p95":486.9838799174398,"p99":563.6399148381553}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:14+02","p50":333.85226675,"p95":484.20084607643696,"p99":519.5828539244461}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:15+02","p50":303.30325650000003,"p95":487.6196518884309,"p99":643.6907188242235}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:16+02","p50":347.79974225,"p95":577.1969705619294,"p99":631.6691500440741}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:17+02","p50":318.02333775,"p95":456.20864617872104,"p99":541.6072872126236}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:18+02","p50":378.190816,"p95":619.9570623480663,"p99":685.133048977197}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:19+02","p50":419.6789895,"p95":645.2635026278042,"p99":811.0415010838723}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:20+02","p50":330.65390875,"p95":600.125367850236,"p99":667.8689065744419}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:21+02","p50":295.71968325,"p95":535.2554408393985,"p99":638.9240327161928}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:22+02","p50":310.5602875,"p95":490.85670221864615,"p99":594.962364588532}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:23+02","p50":307.4521145,"p95":511.08938277472186,"p99":572.715615489068}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:24+02","p50":293.3413045,"p95":534.2967215712881,"p99":651.0559156541538}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:25+02","p50":318.8742605,"p95":499.16811679777,"p99":624.0814159430123}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:26+02","p50":315.435583125,"p95":494.4907585545782,"p99":583.2955762381969}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:27+02","p50":297.67247999999995,"p95":477.88036936586894,"p99":556.6162288245912}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:28+02","p50":324.94858925,"p95":517.539524587432,"p99":583.698400930685}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:29+02","p50":323.78296075,"p95":517.4314450198067,"p99":612.6719742975177}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:30+02","p50":345.84636675,"p95":566.0622260694211,"p99":694.4989783399391}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:31+02","p50":338.43930275,"p95":540.3704727656424,"p99":647.513178981524}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:32+02","p50":315.49332525,"p95":528.8310196370462,"p99":596.4071484533229}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:33+02","p50":320.25945975,"p95":500.6513166889908,"p99":557.4933477221203}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:34+02","p50":324.80278575,"p95":486.9775078169079,"p99":574.1470536415329}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:35+02","p50":293.403228375,"p95":515.0906835788616,"p99":597.309944086947}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:36+02","p50":285.350331375,"p95":487.1931897997964,"p99":553.8792018729384}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:37+02","p50":309.449604,"p95":515.0356002773511,"p99":549.5819831853958}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:38+02","p50":306.8011365,"p95":445.0728730842158,"p99":482.2868831689527}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:39+02","p50":318.655461,"p95":470.20219925142516,"p99":511.0270397748909}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:40+02","p50":310.432168,"p95":514.7370236046787,"p99":551.264536050488}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:41+02","p50":301.745571,"p95":506.9501718176842,"p99":616.5461299818114}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:42+02","p50":318.40706175,"p95":529.5567735206054,"p99":590.7033028432536}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:43+02","p50":312.969415375,"p95":503.445947123498,"p99":631.2859423757071}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:44+02","p50":322.49209924999997,"p95":522.9672847355642,"p99":647.7381676844205}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:45+02","p50":315.16366874999994,"p95":537.4620491556883,"p99":719.7507715904802}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:46+02","p50":323.68091525,"p95":532.9382123968081,"p99":562.3216370969019}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:47+02","p50":346.68994337500004,"p95":584.8454665628483,"p99":744.5853204668167}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:48+02","p50":386.425078,"p95":784.7564577795508,"p99":888.3893254928813}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:49+02","p50":403.349631,"p95":608.9356836375546,"p99":743.9230100532303}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:50+02","p50":351.079016,"p95":642.5210489758571,"p99":770.4706950490704}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:51+02","p50":302.89721,"p95":496.89994052726433,"p99":525.067742970924}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:52+02","p50":322.574773625,"p95":514.2510070523305,"p99":652.4840814783556}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:53+02","p50":337.6034145,"p95":467.2438498147283,"p99":528.269330598588}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:54+02","p50":318.559557625,"p95":457.28120237560074,"p99":574.6904906653624}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:55+02","p50":303.70893375,"p95":549.8322872879548,"p99":628.297075645979}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:56+02","p50":311.8553755,"p95":454.46989390990416,"p99":569.136391301688}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:57+02","p50":320.513333,"p95":515.0829108275394,"p99":575.8809617809062}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:58+02","p50":326.8126315,"p95":470.78010936192743,"p99":505.53433785863115}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:41:59+02","p50":312.40408525,"p95":509.3496198810025,"p99":633.0293030510302}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:00+02","p50":323.30809425,"p95":484.1047928735289,"p99":616.0170981187821}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:01+02","p50":336.66561175000004,"p95":509.280181792151,"p99":561.3578553932957}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:02+02","p50":316.45768325,"p95":528.5621460147039,"p99":586.6443262122751}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:03+02","p50":322.49223674999996,"p95":514.7743987051591,"p99":579.0984374669387}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:04+02","p50":323.482571375,"p95":548.3249511110104,"p99":605.0175722270608}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:05+02","p50":299.96462725000004,"p95":490.0013817073374,"p99":580.3914451528213}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:06+02","p50":334.218077,"p95":503.7678927114556,"p99":565.7659849495211}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:07+02","p50":302.65574200000003,"p95":534.2894679439306,"p99":573.4125380651896}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:08+02","p50":329.6782675,"p95":507.9696080238378,"p99":586.1021280679006}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:09+02","p50":297.7699725,"p95":510.34910517110586,"p99":579.692875771944}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:10+02","p50":316.15952975,"p95":487.4539040628368,"p99":557.6102038947726}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:11+02","p50":341.07417675,"p95":541.9830433137706,"p99":603.3066621361351}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:12+02","p50":330.58212175,"p95":474.92338477380133,"p99":572.1673446765309}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:13+02","p50":331.43706525,"p95":542.5854363821318,"p99":600.7207056061845}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:14+02","p50":308.8410665,"p95":517.7612954149139,"p99":618.871893006094}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:15+02","p50":309.9929055,"p95":541.2307780276724,"p99":671.8582049587717}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:16+02","p50":338.404977625,"p95":545.8150527256497,"p99":614.7325576396663}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:17+02","p50":348.29992825,"p95":507.2583596070805,"p99":565.1056478395903}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:18+02","p50":420.027892,"p95":625.1432876739595,"p99":776.8548635069598}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:19+02","p50":416.82770400000004,"p95":654.5306135843845,"p99":740.6236414797163}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:20+02","p50":330.53776112500003,"p95":581.1028939177265,"p99":653.1910018763497}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:21+02","p50":369.1526625,"p95":596.8604122721432,"p99":700.4589788419195}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:22+02","p50":345.38576450000005,"p95":535.6493107181265,"p99":606.224765239501}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:23+02","p50":309.920321,"p95":487.89770756828415,"p99":603.1206214935105}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:24+02","p50":324.47928575000003,"p95":508.20170975121056,"p99":666.8904040487128}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:25+02","p50":349.18265337500003,"p95":479.5071976475417,"p99":505.8866622045445}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:26+02","p50":330.54887562499994,"p95":450.7554980978613,"p99":489.7120110605159}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:27+02","p50":343.730371875,"p95":493.7631183877071,"p99":555.04478314539}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:28+02","p50":337.99702012500006,"p95":510.4028049346196,"p99":629.3045883811124}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:29+02","p50":339.243583,"p95":537.2696932108249,"p99":587.8088385430145}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:30+02","p50":315.843077,"p95":470.7670652149177,"p99":571.4889124666214}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:31+02","p50":350.03122237499997,"p95":522.2099784214005,"p99":589.0143701041894}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:32+02","p50":326.924258375,"p95":509.3178959582295,"p99":564.8230784894492}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:33+02","p50":316.035223,"p95":522.067869312798,"p99":592.4449681284857}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:34+02","p50":321.66661899999997,"p95":694.2694242244008,"p99":804.5431043952805}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:35+02","p50":308.22360349999997,"p95":514.3069604655512,"p99":595.8185673666338}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:36+02","p50":330.61765,"p95":533.8932072991197,"p99":647.332133349102}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:37+02","p50":350.51526249999995,"p95":511.5237501097942,"p99":582.5538779358175}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:38+02","p50":305.87990575000003,"p95":473.1666702928462,"p99":582.0423681405124}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:39+02","p50":325.0968145,"p95":524.3519463490534,"p99":548.2684647625961}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:40+02","p50":313.85357999999997,"p95":508.5812824218935,"p99":600.2153679181232}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:41+02","p50":309.533420125,"p95":488.89638473662484,"p99":630.9097560657021}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:42+02","p50":336.22685449999994,"p95":468.71740999862425,"p99":548.4321008022217}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:43+02","p50":323.4750845,"p95":510.9212389961422,"p99":587.6782060044155}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:44+02","p50":316.91954875,"p95":528.9628326702832,"p99":679.8281462052851}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:45+02","p50":308.04483700000003,"p95":482.90176352845396,"p99":564.8573764919057}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:46+02","p50":356.40417025,"p95":491.9380949132031,"p99":581.6491912525258}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:47+02","p50":330.98668475,"p95":563.7884276829878,"p99":661.9602661078023}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:48+02","p50":418.76185,"p95":715.4987217449041,"p99":841.5310871147861}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:49+02","p50":445.64414,"p95":770.0418201068027,"p99":906.1397841380792}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:50+02","p50":379.91148699999997,"p95":650.2271047559034,"p99":757.348003292974}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:51+02","p50":334.6581065,"p95":549.8081047190972,"p99":651.5491413281812}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:52+02","p50":332.70984875,"p95":521.9127705617464,"p99":598.6372660789403}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:53+02","p50":333.0945265,"p95":529.5789459835682,"p99":618.296226774334}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:54+02","p50":324.59780025,"p95":556.8821676770366,"p99":688.2358825259287}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:55+02","p50":340.29491575000003,"p95":509.0787693624846,"p99":617.5421699590264}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:56+02","p50":327.57181325,"p95":545.5759211806184,"p99":595.4499519324455}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:57+02","p50":387.08001774999997,"p95":589.778122813007,"p99":710.7826134849253}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:58+02","p50":349.606655875,"p95":564.0891520012915,"p99":664.5532803822157}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:42:59+02","p50":331.03212662500005,"p95":489.86048063179726,"p99":558.7947575490754}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:00+02","p50":321.2447595,"p95":468.42394510229684,"p99":534.6001989460735}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:01+02","p50":328.57371725,"p95":505.8325801870539,"p99":588.3147489771061}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:02+02","p50":324.208074,"p95":539.6714646978564,"p99":633.4924001450978}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:03+02","p50":328.3624995,"p95":504.8007488905276,"p99":577.6911164909186}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:04+02","p50":342.49406200000004,"p95":556.0203131826987,"p99":658.8486427469214}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:05+02","p50":326.3104575,"p95":550.9121454604501,"p99":649.8969540989366}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:06+02","p50":344.91071875,"p95":509.65463865328184,"p99":568.3216354299843}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:07+02","p50":318.521784875,"p95":511.2065145025327,"p99":591.3988925365489}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:08+02","p50":312.596453375,"p95":507.95963390923663,"p99":590.5658738199346}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:09+02","p50":338.92585825000003,"p95":533.7577879749393,"p99":576.3004978027649}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:10+02","p50":336.47683625,"p95":562.383729663859,"p99":599.8609079838258}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:11+02","p50":327.113832625,"p95":517.527400115967,"p99":578.5028320652666}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:12+02","p50":327.3078845,"p95":481.74701875358863,"p99":522.8601290657116}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:13+02","p50":354.90136375,"p95":572.4460333586337,"p99":640.893094044488}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:14+02","p50":330.89827925,"p95":564.9274914541887,"p99":652.2808648848171}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:15+02","p50":347.57848225,"p95":540.4815923843818,"p99":651.1613865393839}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:16+02","p50":340.834904,"p95":569.4828697374805,"p99":692.0377972832339}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:17+02","p50":358.112802,"p95":626.5573374826258,"p99":648.2736702828009}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:18+02","p50":440.42444950000004,"p95":650.9863843936222,"p99":782.4971167358399}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:19+02","p50":413.64021149999996,"p95":747.1482057006587,"p99":917.8008568120785}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:20+02","p50":318.55529125,"p95":587.0976869933598,"p99":650.5547205419783}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:21+02","p50":327.19383750000003,"p95":467.3512144772246,"p99":513.6789322461367}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:22+02","p50":331.60111799999993,"p95":483.6494313948022,"p99":542.9437926127504}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:23+02","p50":340.103423625,"p95":547.2012210528068,"p99":583.4993008570337}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:24+02","p50":349.34509975,"p95":508.2810611659913,"p99":542.3164453763083}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:25+02","p50":342.29770725000003,"p95":541.0706412453068,"p99":578.5239365249439}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:26+02","p50":327.26271862500005,"p95":519.156284634458,"p99":591.2775145309606}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:27+02","p50":321.31289200000003,"p95":550.4422337083914,"p99":727.8195606816721}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:28+02","p50":335.58940375,"p95":526.3931017083185,"p99":624.7924379633237}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:29+02","p50":346.36725175000004,"p95":539.4061676581839,"p99":625.2804323605232}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:30+02","p50":336.19097550000004,"p95":551.3631965409066,"p99":634.8267021399197}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:31+02","p50":342.493862125,"p95":542.1946994635236,"p99":608.1275651678474}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:32+02","p50":343.356515,"p95":543.9354308191412,"p99":585.5432913191397}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:33+02","p50":322.77636037499997,"p95":546.2677592612876,"p99":604.0025447862323}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:34+02","p50":346.44739775,"p95":573.6535181222276,"p99":644.8819079760972}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:35+02","p50":334.605818,"p95":558.9967375222055,"p99":641.1136848389373}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:36+02","p50":314.65849875000004,"p95":486.85783002175623,"p99":506.13328598721125}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:37+02","p50":313.059019375,"p95":513.1926294755925,"p99":574.490563398947}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:38+02","p50":356.152302,"p95":629.7512799691596,"p99":712.5001832770805}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:39+02","p50":350.729707625,"p95":490.0353317711048,"p99":541.0986881269837}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:40+02","p50":321.2167895,"p95":564.3305929822646,"p99":632.0842256974883}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:41+02","p50":315.42502625,"p95":463.52723249894916,"p99":598.9443610534997}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:42+02","p50":322.43123075,"p95":522.2043013386689,"p99":626.181152649395}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:43+02","p50":327.319726,"p95":511.11810669396976,"p99":564.4221545269623}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:44+02","p50":350.81368125,"p95":546.9990960221184,"p99":657.1244605480948}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:45+02","p50":337.696921,"p95":529.0149504819892,"p99":613.8735210176029}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:46+02","p50":357.2893755,"p95":563.1612284092183,"p99":657.4573452784075}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:47+02","p50":339.6019,"p95":582.7254937191329,"p99":629.5898235055847}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:48+02","p50":402.135537,"p95":711.9188947588846,"p99":892.3447114332228}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:49+02","p50":441.45477600000004,"p95":733.9246420231929,"p99":935.1766653473816}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:50+02","p50":406.062491,"p95":760.0104188199185,"p99":834.7430915743317}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:51+02","p50":320.406151875,"p95":543.5071282592572,"p99":576.6741897093175}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:52+02","p50":349.87627725,"p95":514.9634185391819,"p99":609.1754714589429}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:53+02","p50":377.359371625,"p95":598.4808368906163,"p99":675.7592210577669}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:54+02","p50":336.052020625,"p95":569.2151762421723,"p99":683.7743174141632}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:55+02","p50":330.08588075,"p95":529.6835019443193,"p99":641.9929328891768}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:56+02","p50":303.15323575,"p95":510.21732344072774,"p99":681.9766942057275}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:57+02","p50":339.1372965,"p95":509.0658947488823,"p99":591.6627659989495}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:58+02","p50":319.83823225000003,"p95":544.7309274728975,"p99":664.1933552479275}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:43:59+02","p50":351.64510112500005,"p95":528.0506835276024,"p99":668.6147146735203}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:00+02","p50":348.252881625,"p95":543.181292921331,"p99":633.5930048063681}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:01+02","p50":331.607667,"p95":539.2300831935438,"p99":631.455975505971}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:02+02","p50":324.649678625,"p95":544.1680615725603,"p99":677.5085549666525}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:03+02","p50":315.25527325,"p95":555.5250743470641,"p99":609.622951829411}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:04+02","p50":365.672411,"p95":543.0288468875787,"p99":652.3411585622215}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:05+02","p50":342.568414,"p95":512.793715325577,"p99":618.4076284013751}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:06+02","p50":333.78929875,"p95":509.0509641225709,"p99":583.3571742093377}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:07+02","p50":309.444478,"p95":504.94748119617464,"p99":584.7733703681555}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:08+02","p50":358.30726975,"p95":555.707903055953,"p99":639.9491676502342}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:09+02","p50":343.423042125,"p95":528.4406846797634,"p99":650.2524399178006}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:10+02","p50":344.6283565,"p95":517.3996959602098,"p99":584.3293029321632}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:11+02","p50":360.36645450000003,"p95":554.0392444659653,"p99":625.9818996899108}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:12+02","p50":366.55893175,"p95":567.4357889148151,"p99":626.7209425291443}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:13+02","p50":346.86440675,"p95":543.6496711809788,"p99":642.096952304966}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:14+02","p50":353.39149075,"p95":577.5596644226237,"p99":643.319340748392}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:15+02","p50":341.09941225,"p95":488.57378077602004,"p99":554.1846986540642}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:16+02","p50":363.713310375,"p95":573.9259659905728,"p99":694.3959354154669}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:17+02","p50":428.348118,"p95":620.7357997010351,"p99":719.3941255517722}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:18+02","p50":400.901007,"p95":755.4515822323385,"p99":820.3866738953035}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:19+02","p50":371.872225,"p95":672.2152944671152,"p99":771.9139738783016}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:20+02","p50":336.51824462499997,"p95":537.1944035475506,"p99":594.6638948443408}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:21+02","p50":322.795362625,"p95":507.3882789071787,"p99":549.3647344492305}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:22+02","p50":325.145735,"p95":503.24908884989725,"p99":611.0339718718205}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:23+02","p50":342.52409925,"p95":605.2133816231802,"p99":743.5402453059545}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:24+02","p50":326.113772375,"p95":546.3615107799446,"p99":633.9234585743316}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:25+02","p50":358.885657625,"p95":605.5025837244076,"p99":672.8392904294071}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:26+02","p50":335.03479175,"p95":507.94215605371664,"p99":539.8503027720566}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:27+02","p50":320.792308,"p95":502.848236345376,"p99":565.9427686338806}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:28+02","p50":340.089294,"p95":567.740797113977,"p99":625.8843573938685}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:29+02","p50":330.62131000000005,"p95":503.91396347586146,"p99":651.4610301186677}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:30+02","p50":313.853461875,"p95":532.662647136313,"p99":635.7935862359376}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:31+02","p50":352.82864575,"p95":583.297518264915,"p99":672.5121789563389}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:32+02","p50":364.65018524999994,"p95":585.8284566888414,"p99":642.1417981324215}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:33+02","p50":366.73125949999996,"p95":608.3559323920485,"p99":725.2928327889746}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:34+02","p50":346.7205105,"p95":564.2498423319068,"p99":631.0187985758477}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:35+02","p50":357.026326625,"p95":643.3134047584376,"p99":691.3110414690401}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:36+02","p50":338.037225625,"p95":587.7517571396664,"p99":666.2727045889056}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:37+02","p50":332.331228875,"p95":557.5391620648119,"p99":733.9624281525951}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:38+02","p50":348.61139025,"p95":538.1252050438933,"p99":623.5037710384449}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:39+02","p50":350.319776875,"p95":548.902283918745,"p99":582.9400503304043}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:40+02","p50":324.78724625,"p95":468.23032480021186,"p99":590.6577618388824}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:41+02","p50":338.604237625,"p95":496.69199885883165,"p99":580.3399563173156}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:42+02","p50":338.38548525,"p95":514.1438362161766,"p99":585.1993780636466}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:43+02","p50":332.84213387499994,"p95":596.9369607276868,"p99":693.003808062556}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:44+02","p50":328.30492449999997,"p95":553.5345307069192,"p99":613.0522008717575}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:45+02","p50":355.51230275,"p95":536.2873064674928,"p99":629.3372348191824}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:46+02","p50":362.88431024999994,"p95":662.7800251113944,"p99":740.9045094218789}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:47+02","p50":363.03878275,"p95":624.5005102746338,"p99":702.4394397960845}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:48+02","p50":452.140451,"p95":641.6064560464545,"p99":808.9536519214856}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:49+02","p50":460.25323149999997,"p95":684.3763280765312,"p99":787.8054245690737}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:50+02","p50":360.07355974999996,"p95":576.2068855519274,"p99":673.0585240240574}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:51+02","p50":339.56782,"p95":556.4122995985499,"p99":584.4511258181686}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:52+02","p50":340.96431025,"p95":542.1747362165277,"p99":694.0009480606399}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:53+02","p50":346.59112725,"p95":545.775912490787,"p99":592.8181228979234}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:54+02","p50":345.29219962499997,"p95":574.982256025082,"p99":638.1739965908649}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:55+02","p50":339.69288850000004,"p95":536.0492649755479,"p99":587.7214661975629}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:56+02","p50":354.87745574999997,"p95":589.7209300949339,"p99":648.512332062253}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:57+02","p50":399.225588,"p95":606.0997370246735,"p99":762.3074757153626}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:58+02","p50":361.609880375,"p95":587.3312141333265,"p99":649.0144829675403}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:44:59+02","p50":375.4963745,"p95":597.3958477720232,"p99":658.8760655571076}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:00+02","p50":324.150401375,"p95":560.6063027899701,"p99":635.3311170758727}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:01+02","p50":353.793433125,"p95":585.4320197072103,"p99":625.3968822394033}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:02+02","p50":337.484696,"p95":510.8684445387955,"p99":545.4295805129166}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:03+02","p50":347.23752025,"p95":543.4740464520517,"p99":596.8581490632382}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:04+02","p50":346.26664337500006,"p95":558.1099372979429,"p99":737.0618150952544}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:05+02","p50":328.94622749999996,"p95":497.8070499913093,"p99":551.2574102764773}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:06+02","p50":339.4385945,"p95":549.4102414865409,"p99":590.4435919302292}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:07+02","p50":363.29308737499997,"p95":549.8564751976868,"p99":594.4757302067794}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:08+02","p50":333.527347,"p95":602.2613198320062,"p99":668.4757034237766}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:09+02","p50":350.487534,"p95":553.9185693652901,"p99":647.0859333514304}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:10+02","p50":357.864382,"p95":541.1163411903921,"p99":593.630792498826}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:11+02","p50":342.684916,"p95":535.7652182280488,"p99":579.419762448008}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:12+02","p50":366.39387325,"p95":537.5041689847051,"p99":643.3912362304791}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:13+02","p50":350.523786,"p95":523.1297766173515,"p99":606.7601464649315}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:14+02","p50":359.49665175,"p95":552.57607071309,"p99":626.9760799493027}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:15+02","p50":349.344872,"p95":546.5477102665989,"p99":590.8390068376393}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:16+02","p50":376.904857,"p95":676.5819791843779,"p99":819.2726504091942}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:17+02","p50":364.3164845,"p95":633.4672356310823,"p99":772.715747096447}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:18+02","p50":450.038067,"p95":702.4808938677854,"p99":871.8367253081093}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:19+02","p50":410.343016,"p95":643.1498567238561,"p99":759.7305919120329}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:20+02","p50":346.97637737499997,"p95":501.28509446006524,"p99":640.3220917080525}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:21+02","p50":332.378516,"p95":536.7870575605347,"p99":622.6890286930113}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:22+02","p50":335.417158375,"p95":499.1350125774557,"p99":676.754268993287}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:23+02","p50":359.990161625,"p95":595.0249133781737,"p99":706.7902989884295}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:24+02","p50":349.976341375,"p95":517.5214534847408,"p99":602.1449828580975}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:25+02","p50":345.627530375,"p95":537.8296353127979,"p99":614.846616106573}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:26+02","p50":399.3272875,"p95":596.5065870531112,"p99":734.8957623056144}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:27+02","p50":364.2523795,"p95":553.1759960812092,"p99":644.5089362873192}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:28+02","p50":336.62618837499997,"p95":514.3392470972312,"p99":564.6238786615079}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:29+02","p50":358.803955,"p95":551.1398884214469,"p99":598.8946582802253}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:30+02","p50":334.55181775,"p95":510.92113687125885,"p99":557.5052702214638}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:31+02","p50":353.88141775,"p95":520.9150071872731,"p99":587.7909470786}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:32+02","p50":342.56084062499997,"p95":515.0521171364501,"p99":601.0929402814107}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:33+02","p50":324.0906635,"p95":523.2055296571111,"p99":657.5371172251472}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:34+02","p50":347.01409925,"p95":540.2876733650646,"p99":585.7058553339463}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:35+02","p50":327.12255100000004,"p95":521.5998255642677,"p99":610.0326364807672}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:36+02","p50":371.173282625,"p95":523.8652652994544,"p99":600.0573835936073}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:37+02","p50":363.48669525,"p95":580.3229243209925,"p99":640.8220176608982}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:38+02","p50":348.821429625,"p95":503.57228163743963,"p99":551.6865038716606}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:39+02","p50":327.1297095,"p95":533.3160169962772,"p99":603.5995423386149}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:40+02","p50":347.57051150000007,"p95":515.7508554867925,"p99":624.7439295672657}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:41+02","p50":348.17471850000004,"p95":503.79439140520384,"p99":526.4679940250745}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:42+02","p50":342.02924125,"p95":540.9323713551387,"p99":663.7699225116005}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:43+02","p50":346.85442975,"p95":573.970615412692,"p99":671.3220911510086}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:44+02","p50":348.94339,"p95":553.8250227548432,"p99":608.9172629492889}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:45+02","p50":362.167644,"p95":572.6568605889731,"p99":642.5339761294265}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:46+02","p50":449.165789,"p95":649.6537892811198,"p99":806.4412916986909}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:47+02","p50":412.744774,"p95":722.7844726881544,"p99":813.37351721904}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:48+02","p50":442.513619,"p95":695.6580571621427,"p99":764.0540100782699}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:49+02","p50":413.373996,"p95":742.3946704845505,"p99":898.4970946870388}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:50+02","p50":401.431331,"p95":806.9983492125108,"p99":1089.5320209498882}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:51+02","p50":327.89469187500003,"p95":578.4287207270136,"p99":657.2523128917635}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:52+02","p50":337.102387,"p95":576.2936623412663,"p99":660.1718600363254}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:53+02","p50":361.21950712499995,"p95":569.2638940045007,"p99":699.6975276886852}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:54+02","p50":331.06800899999996,"p95":524.4758514649495,"p99":558.5047467185478}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:55+02","p50":364.307729,"p95":546.9215105486043,"p99":609.24503898455}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:56+02","p50":338.17483325,"p95":537.529060036429,"p99":596.3516635225019}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:57+02","p50":335.948761,"p95":524.5735766707137,"p99":649.8923334592514}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:58+02","p50":345.1245805,"p95":551.4527573691249,"p99":689.5070633250904}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:45:59+02","p50":340.35369575,"p95":574.0492728977716,"p99":734.3485056186771}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:00+02","p50":326.01020625,"p95":534.3457177984557,"p99":642.8531181339529}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:01+02","p50":349.556566,"p95":518.3864366541636,"p99":694.0963477342606}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:02+02","p50":360.87075749999997,"p95":579.7477344035956,"p99":680.0530456225418}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:03+02","p50":357.371667,"p95":533.109212540358,"p99":578.6317545747489}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:04+02","p50":341.749813625,"p95":573.3093022196327,"p99":644.7616532399826}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:05+02","p50":320.6170245,"p95":535.0045539217391,"p99":574.1511910854016}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:06+02","p50":349.35370550000005,"p95":615.5754129626853,"p99":664.3451944788022}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:07+02","p50":342.0677505,"p95":500.23475022103133,"p99":546.5653979372277}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:08+02","p50":342.98735500000004,"p95":570.6678997224845,"p99":616.7477293901939}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:09+02","p50":347.29162375,"p95":541.7223988069749,"p99":712.2159962368186}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:10+02","p50":365.035491125,"p95":540.6129649454722,"p99":637.7218718838925}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:11+02","p50":350.03384875,"p95":575.908056891989,"p99":631.3995757880421}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:12+02","p50":338.81277525,"p95":560.0984794637244,"p99":775.6743723776826}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:13+02","p50":347.71150775,"p95":551.2569839859333,"p99":604.849165405223}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:14+02","p50":374.16590175,"p95":548.4767495970355,"p99":572.9626778943658}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:15+02","p50":337.250061625,"p95":506.517735715367,"p99":544.5336736389237}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:16+02","p50":374.160351,"p95":599.6476712737378,"p99":651.4095189924625}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:17+02","p50":391.4443225,"p95":574.6293131787165,"p99":675.6484147643184}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:18+02","p50":422.722661,"p95":801.8904405198309,"p99":863.7027500875104}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:19+02","p50":426.4683285,"p95":723.6471293044243,"p99":884.0684621323076}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:20+02","p50":345.12625475,"p95":583.8862624139952,"p99":661.0541674827142}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:21+02","p50":345.53076375,"p95":536.2967791234223,"p99":637.1758599501028}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:22+02","p50":379.48715925,"p95":564.6169836967816,"p99":652.4221536137562}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:23+02","p50":376.84858725000004,"p95":520.5663124032963,"p99":668.127548019948}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:24+02","p50":351.26797224999996,"p95":544.0758982710404,"p99":647.1024968332835}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:25+02","p50":360.2830105,"p95":575.9003097748199,"p99":664.8352036892582}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:26+02","p50":340.13485075,"p95":559.0044568046784,"p99":618.4696541508598}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:27+02","p50":356.90814687500006,"p95":539.6683896089304,"p99":588.990574314398}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:28+02","p50":364.363302,"p95":557.9573939390931,"p99":714.4486014077387}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:29+02","p50":369.2119467499999,"p95":559.8195483858964,"p99":605.4333882320802}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:30+02","p50":353.59453824999997,"p95":540.943501826767,"p99":604.0178129430085}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:31+02","p50":341.64630150000005,"p95":598.2149039603522,"p99":688.8407177724839}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:32+02","p50":363.850076875,"p95":548.4968659399699,"p99":624.3300117749095}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:33+02","p50":320.93730625,"p95":565.3146409970316,"p99":671.0347354925132}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:34+02","p50":370.12893325,"p95":585.4740319925847,"p99":650.0584546206627}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:35+02","p50":361.22903125,"p95":537.0443487406942,"p99":618.8847951454678}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:36+02","p50":382.79561574999997,"p95":522.8193759887354,"p99":591.3114227743597}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:37+02","p50":345.733393125,"p95":641.1866238535613,"p99":699.9755659561661}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:38+02","p50":353.763625875,"p95":547.664618671233,"p99":623.113616392451}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:39+02","p50":367.84983475,"p95":538.0287704812815,"p99":603.2834892632761}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:40+02","p50":329.19345899999996,"p95":517.658193310068,"p99":596.2428623315711}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:41+02","p50":378.69003875,"p95":557.4398456603016,"p99":633.9591832523568}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:42+02","p50":348.52142287500004,"p95":519.0831945842875,"p99":561.6199110145434}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:43+02","p50":354.59012375,"p95":525.9576285781751,"p99":665.4675615167932}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:44+02","p50":381.149179,"p95":570.1894354293403,"p99":674.8120213161407}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:45+02","p50":376.36328249999997,"p95":534.1634561133704,"p99":607.3743401134548}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:46+02","p50":375.87968712500003,"p95":607.4298005219961,"p99":754.2779052279494}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:47+02","p50":367.839692,"p95":614.6653641036158,"p99":684.1563988919105}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:48+02","p50":470.60785,"p95":705.6385812547643,"p99":841.9683077408989}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:49+02","p50":461.235327,"p95":805.8623452664671,"p99":896.7389074969918}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:50+02","p50":410.10131075000004,"p95":685.3478531655312,"p99":824.0622178336725}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:51+02","p50":327.795443,"p95":547.0594844745756,"p99":619.2649680240106}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:52+02","p50":354.61930975,"p95":558.4728557765573,"p99":636.97769057809}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:53+02","p50":345.214233375,"p95":527.2921589568558,"p99":558.6288495968028}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:54+02","p50":350.352511375,"p95":503.8315707582582,"p99":609.9543784524154}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:55+02","p50":363.90394137500004,"p95":572.6521485627101,"p99":665.6983828493146}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:56+02","p50":365.492157,"p95":523.3605533984289,"p99":571.8051118889975}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:57+02","p50":371.169569,"p95":523.2952542524438,"p99":571.2109034612259}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:58+02","p50":436.911162,"p95":638.304762745309,"p99":709.4670206928821}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:46:59+02","p50":411.18073,"p95":612.3270736110799,"p99":715.6366150032653}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:00+02","p50":352.9105875,"p95":540.8583767316579,"p99":621.0581579829368}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:01+02","p50":376.91366800000003,"p95":608.9526297512717,"p99":633.7185279041666}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:02+02","p50":374.34576412499996,"p95":595.1244913522681,"p99":627.2598151529543}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:03+02","p50":356.11296812500007,"p95":534.0915329685939,"p99":612.1283232763257}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:04+02","p50":373.62495075000004,"p95":571.1913684215087,"p99":599.2966416379137}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:05+02","p50":349.42042949999995,"p95":629.4699964729137,"p99":722.7621205956013}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:06+02","p50":376.271132,"p95":591.6972141875145,"p99":688.8064591543424}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:07+02","p50":363.15761062499996,"p95":580.7751883201435,"p99":687.49893791833}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:08+02","p50":359.12589075000005,"p95":627.3962238782893,"p99":771.416657282438}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:09+02","p50":331.537497875,"p95":552.8859099029235,"p99":667.0232644884268}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:10+02","p50":344.60761149999996,"p95":670.7708161270356,"p99":732.9215999619885}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:11+02","p50":329.5246875,"p95":582.709590824389,"p99":722.9900399842286}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:12+02","p50":365.487416,"p95":560.7896041474411,"p99":621.9079438566717}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:13+02","p50":366.85857175,"p95":565.3370858114414,"p99":654.078266817527}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:14+02","p50":376.624474125,"p95":600.2568704768521,"p99":654.9918930514531}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:15+02","p50":385.75254862500003,"p95":578.4678990606368,"p99":641.6020224810624}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:16+02","p50":366.97081049999997,"p95":647.33773638466,"p99":743.7291821785717}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:17+02","p50":393.7892745,"p95":618.3646556576285,"p99":698.0266578596397}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:18+02","p50":431.89287,"p95":756.3606272760418,"p99":857.3252262805039}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:19+02","p50":502.68915549999997,"p95":795.9049426001947,"p99":934.9490622697764}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:20+02","p50":420.67546100000004,"p95":674.8818829643311,"p99":793.2769937691173}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:21+02","p50":355.45911025,"p95":563.6235681177752,"p99":665.7891341768741}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:22+02","p50":351.042864,"p95":550.1976598919637,"p99":624.438729203361}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:23+02","p50":336.83872825,"p95":535.7430034556264,"p99":700.4418950208094}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:24+02","p50":382.423903,"p95":595.7934666937183,"p99":652.1804639763667}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:25+02","p50":356.95381125,"p95":588.686524896492,"p99":675.3769710497913}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:26+02","p50":342.142293375,"p95":527.0026947642408,"p99":658.6139797398522}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:27+02","p50":351.54080849999997,"p95":598.2845428583211,"p99":640.1780923918863}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:28+02","p50":351.10330275,"p95":568.0761515172472,"p99":618.3176152456906}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:29+02","p50":382.7587185,"p95":602.253188061808,"p99":655.8339036481781}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:30+02","p50":356.3262155,"p95":576.6441496147552,"p99":662.9474969482193}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:31+02","p50":377.2490755,"p95":546.042730181945,"p99":592.7406988252216}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:32+02","p50":366.4939325,"p95":575.1407336788749,"p99":658.0646213520932}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:33+02","p50":334.68827450000003,"p95":550.26795746339,"p99":610.1698392888622}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:34+02","p50":368.598075,"p95":605.5791265990532,"p99":672.2396189265404}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:35+02","p50":362.02651275,"p95":519.6200709826431,"p99":580.9427359890051}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:36+02","p50":367.76984849999997,"p95":587.425418991338,"p99":638.4740805902625}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:37+02","p50":360.245454375,"p95":608.7418731459536,"p99":653.1512138291268}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:38+02","p50":406.948706,"p95":607.1203332223666,"p99":640.8209670412433}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:39+02","p50":391.03764775,"p95":548.5080014495067,"p99":656.0194364837594}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:40+02","p50":407.506433,"p95":641.3765128161691,"p99":693.6325246361919}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:41+02","p50":392.0926725,"p95":606.018242698949,"p99":677.4727272443628}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:42+02","p50":380.680381,"p95":580.4023302434218,"p99":685.8592293568843}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:43+02","p50":371.282735,"p95":577.8334548774365,"p99":667.6755771671204}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:44+02","p50":381.125060125,"p95":600.6462395270736,"p99":728.9498561144115}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:45+02","p50":364.24978,"p95":546.2749676986276,"p99":615.2926799143088}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:46+02","p50":394.563987875,"p95":589.5063062651146,"p99":652.27759246698}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:47+02","p50":398.128657,"p95":620.3325964756989,"p99":708.7442627269784}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:48+02","p50":489.1820475,"p95":720.4108372897692,"p99":886.7874097963199}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:49+02","p50":445.4518175,"p95":728.9021142257258,"p99":949.7441621377039}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:50+02","p50":480.368007,"p95":793.0226181554071,"p99":971.231016473387}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:51+02","p50":370.2256745,"p95":524.9930269471047,"p99":632.8595963865823}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:52+02","p50":382.141342,"p95":577.2822490433972,"p99":682.8809792750768}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:53+02","p50":351.329848,"p95":582.114161490897,"p99":671.9360361640039}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:54+02","p50":366.29761499999995,"p95":565.6847946252662,"p99":673.3247704165912}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:55+02","p50":360.08347075,"p95":608.0463271750035,"p99":709.0654027889042}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:56+02","p50":387.609815,"p95":597.6089246492459,"p99":687.7932023599882}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:57+02","p50":378.806582,"p95":563.3652127297133,"p99":627.0161513699355}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:58+02","p50":367.4258025,"p95":553.874677023038,"p99":702.2397284850064}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:47:59+02","p50":385.534227,"p95":567.5638756617304,"p99":658.5302650030661}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:00+02","p50":358.108514375,"p95":592.2173371127257,"p99":647.7260890681663}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:01+02","p50":372.657969,"p95":650.653177755713,"p99":722.1162193479786}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:02+02","p50":374.977187,"p95":561.7995290611792,"p99":638.3017917092704}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:03+02","p50":384.11512424999995,"p95":554.6637274162941,"p99":596.0777641940341}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:04+02","p50":345.30697775000004,"p95":647.6057250044437,"p99":733.7630055309344}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:05+02","p50":371.919690375,"p95":538.4030222033645,"p99":666.8601673203542}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:06+02","p50":370.85242025,"p95":566.8579537161854,"p99":596.3000648483257}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:07+02","p50":371.3460725,"p95":600.864875038826,"p99":696.5931488108387}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:08+02","p50":377.0203545,"p95":593.0985303793224,"p99":717.2182938407545}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:09+02","p50":350.117952625,"p95":551.1117659721687,"p99":629.2865839315465}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:10+02","p50":384.89380324999996,"p95":557.8008546511566,"p99":636.580461088732}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:11+02","p50":366.029803,"p95":564.3884609931798,"p99":661.2605684219055}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:12+02","p50":359.28891,"p95":561.3114050814762,"p99":670.2915985139274}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:13+02","p50":368.3550525,"p95":528.7123712326683,"p99":660.9452924902478}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:14+02","p50":403.413705,"p95":647.2213528704553,"p99":787.9417279004001}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:15+02","p50":316.89532575,"p95":585.3375307634708,"p99":746.9125202290841}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:16+02","p50":366.46199775,"p95":564.5855855665229,"p99":720.3569641328735}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:17+02","p50":395.0455565,"p95":630.3617309191131,"p99":712.5628947219238}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:18+02","p50":452.543411,"p95":666.0552077854462,"p99":818.2982870411587}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:19+02","p50":475.983971,"p95":815.1069785751895,"p99":992.4595084467697}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:20+02","p50":396.29265350000003,"p95":760.2994612084045,"p99":881.2400988808289}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:21+02","p50":350.0927785,"p95":534.3606522786283,"p99":685.6315452478809}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:22+02","p50":355.531666,"p95":562.436802332802,"p99":655.665564886801}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:23+02","p50":379.008702,"p95":526.9116277446686,"p99":579.3579618329161}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:24+02","p50":369.6668505,"p95":562.9086056966726,"p99":642.9324887840419}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:25+02","p50":374.783023125,"p95":556.1949239988519,"p99":648.6149735306576}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:26+02","p50":373.97536675,"p95":545.8640974620236,"p99":603.0847192951051}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:27+02","p50":357.790776875,"p95":592.9894363913226,"p99":698.608809728214}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:28+02","p50":380.39239150000003,"p95":560.935014845125,"p99":713.1072916162286}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:29+02","p50":375.62008249999997,"p95":549.8600039423752,"p99":614.5286269966126}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:30+02","p50":354.73620475,"p95":518.6924974618819,"p99":653.8115801267328}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:31+02","p50":408.81667400000003,"p95":580.7742718071449,"p99":743.639358516593}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:32+02","p50":373.717473,"p95":591.7884088895145,"p99":653.9233233542363}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:33+02","p50":457.6231675,"p95":713.9853752811575,"p99":801.5314229417592}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:34+02","p50":398.69369099999994,"p95":576.9453266908774,"p99":638.878944097023}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:35+02","p50":380.10456575,"p95":575.6161159108634,"p99":609.1460214958057}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:36+02","p50":390.482404,"p95":564.0768451614668,"p99":716.9423898276623}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:37+02","p50":376.32667000000004,"p95":600.187044783256,"p99":781.7169870735951}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:38+02","p50":387.6777615,"p95":560.6641300469131,"p99":595.6967540945435}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:39+02","p50":402.231564125,"p95":530.5324569812227,"p99":632.398123102675}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:40+02","p50":387.8637995,"p95":594.2622456500368,"p99":640.2609036061725}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:41+02","p50":372.434851,"p95":524.2710856375901,"p99":578.9491680272847}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:42+02","p50":387.522010625,"p95":580.4659249738294,"p99":711.8193150877477}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:43+02","p50":387.458622,"p95":620.741373132667,"p99":658.9881203480729}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:44+02","p50":392.4298125,"p95":629.7071045733506,"p99":705.2516679337415}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:45+02","p50":376.85462225,"p95":533.5347648917531,"p99":639.7289377307749}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:46+02","p50":395.706921,"p95":641.9824354647773,"p99":702.8896531357329}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:47+02","p50":376.69207400000005,"p95":657.763545992053,"p99":805.5041914577865}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:48+02","p50":509.502717,"p95":755.5429311040151,"p99":935.4421637327413}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:49+02","p50":474.339925,"p95":745.6588229550018,"p99":864.3030752003326}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:50+02","p50":416.080606,"p95":740.9167848159949,"p99":896.6391244707446}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:51+02","p50":374.4881395,"p95":568.7187625705914,"p99":640.6493812109198}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:52+02","p50":377.6801165,"p95":627.671924471273,"p99":727.8040352599335}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:53+02","p50":407.80580174999994,"p95":637.1368828413749,"p99":749.3144091134742}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:54+02","p50":390.149391,"p95":532.2912166657791,"p99":621.3468670521851}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:55+02","p50":353.1494325,"p95":578.9051062667124,"p99":622.0327491599936}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:56+02","p50":380.552572,"p95":573.046774797695,"p99":643.197298432375}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:57+02","p50":397.34807575,"p95":601.8214968569139,"p99":737.5489854612472}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:58+02","p50":379.87111775,"p95":601.284545349459,"p99":641.9105274041752}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:48:59+02","p50":372.430882,"p95":574.5653418701997,"p99":656.5257328923083}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:00+02","p50":391.869787,"p95":594.046199838078,"p99":661.4024955055589}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:01+02","p50":380.62370150000004,"p95":601.2354013430615,"p99":699.7857347286258}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:02+02","p50":381.049654,"p95":606.5920663715998,"p99":664.5980062494796}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:03+02","p50":383.42459199999996,"p95":620.6149392064591,"p99":717.3536460669555}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:04+02","p50":394.31672,"p95":608.5648653262712,"p99":752.5454078678036}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:05+02","p50":393.24571000000003,"p95":628.0916439125399,"p99":697.0547843593827}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:06+02","p50":403.32300975,"p95":600.9828152580207,"p99":705.8764485959186}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:07+02","p50":381.233005,"p95":593.6824948727258,"p99":646.7162360751807}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:08+02","p50":378.452192125,"p95":585.3596847937857,"p99":658.7346346348761}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:09+02","p50":406.263443,"p95":601.8851923236006,"p99":657.2011940190091}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:10+02","p50":379.06424675,"p95":536.3294893311079,"p99":657.037147603148}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:11+02","p50":385.3275725,"p95":601.5528484018837,"p99":655.0453609955755}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:12+02","p50":364.032396875,"p95":653.4345941447176,"p99":756.3619943769318}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:13+02","p50":375.1136185,"p95":563.4905159659334,"p99":609.4856272265067}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:14+02","p50":374.0972925,"p95":603.8206581368601,"p99":665.2039553373108}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:15+02","p50":387.637838,"p95":577.552652499797,"p99":693.657785178182}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:16+02","p50":395.9279765,"p95":573.2520221783011,"p99":623.4036501542654}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:17+02","p50":385.011343,"p95":578.0024207123759,"p99":739.5986926926594}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:18+02","p50":501.070556,"p95":739.1737046452372,"p99":817.5306489707167}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:19+02","p50":487.48740399999997,"p95":714.5395390926317,"p99":788.6833549983197}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:20+02","p50":398.6928585,"p95":624.9289234712396,"p99":735.1492829041281}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:21+02","p50":381.2014705,"p95":552.4190413578395,"p99":607.1950033001709}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:22+02","p50":378.4648505,"p95":572.1401214373743,"p99":672.7073571081388}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:23+02","p50":381.756316,"p95":546.8975953816202,"p99":656.8522043579336}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:24+02","p50":398.69236900000004,"p95":564.599991420596,"p99":666.2908761377087}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:25+02","p50":369.738289,"p95":591.8191478315905,"p99":667.6489562640949}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:26+02","p50":374.1937285,"p95":561.258875386296,"p99":635.1006114370289}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:27+02","p50":364.389940375,"p95":607.656058991329,"p99":638.7479768448577}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:28+02","p50":392.7941255,"p95":582.934002763613,"p99":668.4763312391167}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:29+02","p50":395.1537375,"p95":581.2645812752047,"p99":629.3970892682247}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:30+02","p50":378.461375,"p95":560.356492213997,"p99":644.3948260291138}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:31+02","p50":382.706153,"p95":554.023332663457,"p99":595.6850969616621}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:32+02","p50":378.17704625,"p95":596.8606748886185,"p99":652.8597620553479}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:33+02","p50":361.20687,"p95":554.0451086429039,"p99":665.8597491082382}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:34+02","p50":375.875681,"p95":573.009036371263,"p99":677.5763840911723}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:35+02","p50":398.95089375,"p95":551.1305156222186,"p99":625.1852731621457}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:36+02","p50":403.060358,"p95":578.952155218065,"p99":617.1697987665615}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:37+02","p50":358.56926925,"p95":602.9619980526259,"p99":706.9007030513372}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:38+02","p50":369.853585875,"p95":558.05031432408,"p99":656.9718285308442}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:39+02","p50":376.7368085,"p95":541.9252990983949,"p99":597.4204704358468}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:40+02","p50":374.443547,"p95":610.7528968129975,"p99":718.2325440708454}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:41+02","p50":390.63388,"p95":566.5547698392392,"p99":751.4906429859648}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:42+02","p50":386.25493274999997,"p95":558.2470035515831,"p99":651.1849266849058}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:43+02","p50":386.3001798749999,"p95":568.7246794361353,"p99":680.0371702720599}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:44+02","p50":414.451416,"p95":582.8826341209705,"p99":688.0970412610598}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:45+02","p50":396.874899,"p95":573.0194268884987,"p99":673.6045943448377}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:46+02","p50":442.434258,"p95":598.4165902697932,"p99":720.8006863922512}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:47+02","p50":403.353726,"p95":548.8214058317883,"p99":825.8187105583257}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:48+02","p50":512.466077,"p95":732.5617006177514,"p99":875.0522002655697}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:49+02","p50":498.12606500000004,"p95":711.3046155963399,"p99":893.5885908343587}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:50+02","p50":419.23831099999995,"p95":653.8646418435065,"p99":740.4648088355135}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:51+02","p50":376.37620575,"p95":552.512115076958,"p99":619.9807175853116}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:52+02","p50":390.14741749999996,"p95":588.6646891362872,"p99":655.2341834101849}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:53+02","p50":400.861338,"p95":577.2982991460228,"p99":600.915501021375}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:54+02","p50":379.451339125,"p95":575.3869842699014,"p99":657.3521219556268}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:55+02","p50":407.52232649999996,"p95":604.6002093730469,"p99":653.4454181223144}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:56+02","p50":363.082460375,"p95":540.8862117627307,"p99":679.5614941885009}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:57+02","p50":365.796288125,"p95":567.4904118875198,"p99":652.1854918458216}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:58+02","p50":372.912214,"p95":562.0312605474297,"p99":632.4050374625965}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:49:59+02","p50":369.301701,"p95":593.6903148726128,"p99":677.3486074447236}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:00+02","p50":421.84070199999996,"p95":580.7411737502654,"p99":621.4996571411447}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:01+02","p50":392.687742,"p95":617.2486047475122,"p99":686.2983731112414}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:02+02","p50":383.580548,"p95":617.7467801069415,"p99":707.103503131856}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:03+02","p50":386.919284,"p95":593.5768893762719,"p99":679.0635569386003}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:04+02","p50":451.1858505,"p95":710.2390074153294,"p99":824.9659940393234}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:05+02","p50":386.025572,"p95":683.426417292915,"p99":896.3165627046494}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:06+02","p50":384.505966,"p95":589.1070330830125,"p99":694.5420661562305}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:07+02","p50":399.1031545,"p95":592.4421404059601,"p99":675.8345912961425}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:08+02","p50":394.995577,"p95":604.3202049969374,"p99":674.6714073621386}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:09+02","p50":367.20676249999997,"p95":585.9666742111297,"p99":659.7609447760358}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:10+02","p50":385.432987,"p95":552.5380288014563,"p99":639.7719383094694}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:11+02","p50":404.909684,"p95":600.2965685052718,"p99":678.1246169718006}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:12+02","p50":400.76854000000003,"p95":609.4308743893686,"p99":640.4930314384975}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:13+02","p50":380.7047025,"p95":576.3048701383278,"p99":672.3878423597536}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:14+02","p50":382.82686674999997,"p95":582.7837581255008,"p99":661.5016473044512}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:15+02","p50":378.88070849999997,"p95":584.1112044746536,"p99":627.313500304875}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:16+02","p50":403.2571525,"p95":632.6679983820713,"p99":682.0945478748445}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:17+02","p50":413.427646,"p95":606.5683102387396,"p99":738.8084224646201}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:18+02","p50":488.05521,"p95":703.4761239822246,"p99":768.0045405116186}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:19+02","p50":526.1706945,"p95":757.2231546335336,"p99":822.1608808752136}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:20+02","p50":428.938374,"p95":735.4151300372282,"p99":845.5614114277008}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:21+02","p50":389.45504625,"p95":642.9555771926074,"p99":747.944182983573}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:22+02","p50":421.3969125,"p95":613.8132536197404,"p99":715.3682359889335}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:23+02","p50":407.713257,"p95":575.4587298318987,"p99":654.8682521849261}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:24+02","p50":406.7370325,"p95":612.7913894636231,"p99":682.8240717960119}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:25+02","p50":414.999551,"p95":679.9491651280817,"p99":712.1398508435125}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:26+02","p50":400.381467,"p95":629.3210713000857,"p99":757.5066780656276}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:27+02","p50":403.815541,"p95":601.0086458383829,"p99":731.782436562891}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:28+02","p50":436.904756,"p95":585.4005056937589,"p99":624.0598723059244}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:29+02","p50":433.925698,"p95":586.8056523985105,"p99":627.1120268679717}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:30+02","p50":416.318149,"p95":604.0334259981772,"p99":670.8958782901761}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:31+02","p50":404.17097,"p95":609.5917478201899,"p99":678.6095126874237}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:32+02","p50":406.893869,"p95":564.5321166061897,"p99":685.2356767854099}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:33+02","p50":390.560496,"p95":565.1381103080156,"p99":639.992465843625}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:34+02","p50":405.088062,"p95":584.9710610011844,"p99":642.5561481885514}, + {"metric_name":"oidc_token_duration","timestamp":"2024-10-22 18:50:35+02","p50":379.604884,"p95":610.3624684022362,"p99":696.5676583588892} +] diff --git a/docs/package.json b/docs/package.json index 3d7b2fe036..e322206563 100644 --- a/docs/package.json +++ b/docs/package.json @@ -44,6 +44,7 @@ "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", + "react-google-charts": "^5.2.1", "react-player": "^2.15.1", "sitemap": "7.1.1", "swc-loader": "^0.2.3", diff --git a/docs/sidebars.js b/docs/sidebars.js index 8c83f9bf1d..be96b61706 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -841,6 +841,30 @@ module.exports = { label: "Rate Limits (Cloud)", // The link label href: "/legal/policies/rate-limit-policy", // The internal path }, + { + type: "category", + label: "Benchmarks", + collapsed: false, + link: { + type: "doc", + id: "apis/benchmarks/index", + }, + items: [ + { + type: "category", + label: "v2.65.0", + link: { + title: "v2.65.0", + slug: "/apis/benchmarks/v2.65.0", + description: + "Benchmark results of Zitadel v2.65.0\n" + }, + items: [ + "apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index", + ], + }, + ], + }, ], selfHosting: [ { diff --git a/docs/src/components/benchmark_chart.jsx b/docs/src/components/benchmark_chart.jsx new file mode 100644 index 0000000000..f9ac920cad --- /dev/null +++ b/docs/src/components/benchmark_chart.jsx @@ -0,0 +1,45 @@ +import React from "react"; +import Chart from "react-google-charts"; + +export function BenchmarkChart(testResults=[], height='500px') { + + const options = { + legend: { position: 'bottom' }, + focusTarget: 'category', + hAxis: { + title: 'timestamp', + }, + vAxis: { + title: 'latency (ms)', + }, + }; + + const data = [ + [ + {type:"datetime", label: "timestamp"}, + {type:"number", label: "p50"}, + {type:"number", label: "p95"}, + {type:"number", label: "p99"}, + ], + ] + + JSON.parse(testResults.testResults).forEach((result) => { + data.push([ + new Date(result.timestamp), + result.p50, + result.p95, + result.p99, + ]) + }); + + return ( + + ); +} \ No newline at end of file diff --git a/docs/static/img/benchmark/Flowchart.svg b/docs/static/img/benchmark/Flowchart.svg new file mode 100644 index 0000000000..e2a078ab96 --- /dev/null +++ b/docs/static/img/benchmark/Flowchart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/yarn.lock b/docs/yarn.lock index 538212d391..b5d92b98b4 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -9479,6 +9479,11 @@ react-fast-compare@^3.0.1, react-fast-compare@^3.2.0, react-fast-compare@^3.2.2: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== +react-google-charts@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-google-charts/-/react-google-charts-5.2.1.tgz#d9cbe8ed45d7c0fafefea5c7c3361bee76648454" + integrity sha512-mCbPiObP8yWM5A9ogej7Qp3/HX4EzOwuEzUYvcfHtL98Xt4V/brD14KgfDzSNNtyD48MNXCpq5oVaYKt0ykQUQ== + react-helmet-async@*: version "2.0.5" resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-2.0.5.tgz#cfc70cd7bb32df7883a8ed55502a1513747223ec" From 5a85c3eda854282346ebf087d70d2a7f7c468441 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:20:03 +0000 Subject: [PATCH 13/32] chore(deps): bump http-proxy-middleware from 2.0.6 to 2.0.7 in /console (#8823) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.7.
Release notes

Sourced from http-proxy-middleware's releases.

v2.0.7

Full Changelog: https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7

v2.0.7-beta.1

Full Changelog: https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7-beta.0...v2.0.7-beta.1

v2.0.7-beta.0

Full Changelog: https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7-beta.0

Changelog

Sourced from http-proxy-middleware's changelog.

v2.0.7

  • ci(github actions): add publish.yml
  • fix(filter): handle errors
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=http-proxy-middleware&package-manager=npm_and_yarn&previous-version=2.0.6&new-version=2.0.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/zitadel/zitadel/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Max Peintner --- console/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/console/yarn.lock b/console/yarn.lock index a820c97d73..74e8599a93 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -6042,9 +6042,9 @@ http-proxy-agent@^5.0.0: debug "4" http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" From 522c82876f4360135bdf4b772ac03669fd9c8e26 Mon Sep 17 00:00:00 2001 From: Silvan Date: Mon, 18 Nov 2024 16:30:12 +0100 Subject: [PATCH 14/32] fix(eventstore): set application name during push to instance id (#8918) # Which Problems Are Solved Noisy neighbours can introduce projection latencies because the projections only query events older than the start timestamp of the oldest push transaction. # How the Problems Are Solved During push we set the application name to `zitadel_es_pusher_` instead of `zitadel_es_pusher` which is used to query events by projections. --- internal/eventstore/repository/sql/crdb.go | 9 ++-- internal/eventstore/repository/sql/query.go | 12 +++++ .../eventstore/repository/sql/query_test.go | 44 +++++++++---------- internal/eventstore/v3/push.go | 18 ++++++++ 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go index 1b3e17377c..68610676c3 100644 --- a/internal/eventstore/repository/sql/crdb.go +++ b/internal/eventstore/repository/sql/crdb.go @@ -15,7 +15,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -125,11 +124,11 @@ type CRDB struct { func NewCRDB(client *database.DB) *CRDB { switch client.Type() { case "cockroach": - awaitOpenTransactionsV1 = " AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = '" + dialect.EventstorePusherAppName + "')" - awaitOpenTransactionsV2 = ` AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = '` + dialect.EventstorePusherAppName + `')` + awaitOpenTransactionsV1 = " AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))" + awaitOpenTransactionsV2 = ` AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))` case "postgres": - awaitOpenTransactionsV1 = ` AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = '` + dialect.EventstorePusherAppName + `' AND state <> 'idle')` - awaitOpenTransactionsV2 = ` AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = '` + dialect.EventstorePusherAppName + `' AND state <> 'idle')` + awaitOpenTransactionsV1 = ` AND EXTRACT(EPOCH FROM created_at) < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` + awaitOpenTransactionsV2 = ` AND "position" < (SELECT COALESCE(EXTRACT(EPOCH FROM min(xact_start)), EXTRACT(EPOCH FROM now())) FROM pg_stat_activity WHERE datname = current_database() AND application_name = ANY(?) AND state <> 'idle')` } return &CRDB{client} diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index bbc9513864..8ecea6e3d6 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -272,7 +272,19 @@ func prepareConditions(criteria querier, query *repository.SearchQuery, useV1 bo } if query.AwaitOpenTransactions { + instanceIDs := make(database.TextArray[string], 0, 3) + if query.InstanceID != nil { + instanceIDs = append(instanceIDs, query.InstanceID.Value.(string)) + } else if query.InstanceIDs != nil { + instanceIDs = append(instanceIDs, query.InstanceIDs.Value.(database.TextArray[string])...) + } + + for i := range instanceIDs { + instanceIDs[i] = dialect.DBPurposeEventPusher.AppName() + "_" + instanceIDs[i] + } + clauses += awaitOpenTransactions(useV1) + args = append(args, instanceIDs) } if clauses == "" { diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 906c153deb..2956c39bd5 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -402,8 +402,8 @@ func Test_prepareCondition(t *testing.T) { useV1: true, }, res: res{ - clause: " WHERE aggregate_type = ANY(?) AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = 'zitadel_es_pusher')", - values: []interface{}{[]eventstore.AggregateType{"user", "org"}}, + clause: " WHERE aggregate_type = ANY(?) AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))", + values: []interface{}{[]eventstore.AggregateType{"user", "org"}, database.TextArray[string]{}}, }, }, { @@ -419,8 +419,8 @@ func Test_prepareCondition(t *testing.T) { }, }, res: res{ - clause: ` WHERE aggregate_type = ANY(?) AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = 'zitadel_es_pusher')`, - values: []interface{}{[]eventstore.AggregateType{"user", "org"}}, + clause: ` WHERE aggregate_type = ANY(?) AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))`, + values: []interface{}{[]eventstore.AggregateType{"user", "org"}, database.TextArray[string]{}}, }, }, { @@ -439,8 +439,8 @@ func Test_prepareCondition(t *testing.T) { useV1: true, }, res: res{ - clause: " WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = 'zitadel_es_pusher')", - values: []interface{}{[]eventstore.AggregateType{"user", "org"}, "1234", []eventstore.EventType{"user.created", "org.created"}}, + clause: " WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND creation_date::TIMESTAMP < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))", + values: []interface{}{[]eventstore.AggregateType{"user", "org"}, "1234", []eventstore.EventType{"user.created", "org.created"}, database.TextArray[string]{}}, }, }, { @@ -458,8 +458,8 @@ func Test_prepareCondition(t *testing.T) { }, }, res: res{ - clause: ` WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = 'zitadel_es_pusher')`, - values: []interface{}{[]eventstore.AggregateType{"user", "org"}, "1234", []eventstore.EventType{"user.created", "org.created"}}, + clause: ` WHERE aggregate_type = ANY(?) AND aggregate_id = ? AND event_type = ANY(?) AND hlc_to_timestamp("position") < (SELECT COALESCE(MIN(start), NOW())::TIMESTAMP FROM crdb_internal.cluster_transactions where application_name = ANY(?))`, + values: []interface{}{[]eventstore.AggregateType{"user", "org"}, "1234", []eventstore.EventType{"user.created", "org.created"}, database.TextArray[string]{}}, }, }, } @@ -687,8 +687,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = 'zitadel_es_pusher'\) ORDER BY event_sequence DESC`, - []driver.Value{eventstore.AggregateType("user")}, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, ), }, res: res{ @@ -709,8 +709,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = 'zitadel_es_pusher'\) ORDER BY event_sequence LIMIT \$2`, - []driver.Value{eventstore.AggregateType("user"), uint64(5)}, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence LIMIT \$3`, + []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, ), }, res: res{ @@ -731,8 +731,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = 'zitadel_es_pusher'\) ORDER BY event_sequence DESC LIMIT \$2`, - []driver.Value{eventstore.AggregateType("user"), uint64(5)}, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC LIMIT \$3`, + []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, ), }, res: res{ @@ -754,8 +754,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events AS OF SYSTEM TIME '-1 ms' WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = 'zitadel_es_pusher'\) ORDER BY event_sequence DESC LIMIT \$2`, - []driver.Value{eventstore.AggregateType("user"), uint64(5)}, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events AS OF SYSTEM TIME '-1 ms' WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC LIMIT \$3`, + []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}, uint64(5)}, ), }, res: res{ @@ -776,8 +776,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQueryErr(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = 'zitadel_es_pusher'\) ORDER BY event_sequence DESC`, - []driver.Value{eventstore.AggregateType("user")}, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, sql.ErrConnDone), }, res: res{ @@ -798,8 +798,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQueryScanErr(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = 'zitadel_es_pusher'\) ORDER BY event_sequence DESC`, - []driver.Value{eventstore.AggregateType("user")}, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$2\)\) ORDER BY event_sequence DESC`, + []driver.Value{eventstore.AggregateType("user"), database.TextArray[string]{}}, &repository.Event{Seq: 100}), }, res: res{ @@ -832,8 +832,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery(t, - `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE \(aggregate_type = \$1 OR \(aggregate_type = \$2 AND aggregate_id = \$3\)\) AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = 'zitadel_es_pusher'\) ORDER BY event_sequence DESC LIMIT \$4`, - []driver.Value{eventstore.AggregateType("user"), eventstore.AggregateType("org"), "asdf42", uint64(5)}, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE \(aggregate_type = \$1 OR \(aggregate_type = \$2 AND aggregate_id = \$3\)\) AND creation_date::TIMESTAMP < \(SELECT COALESCE\(MIN\(start\), NOW\(\)\)::TIMESTAMP FROM crdb_internal\.cluster_transactions where application_name = ANY\(\$4\)\) ORDER BY event_sequence DESC LIMIT \$5`, + []driver.Value{eventstore.AggregateType("user"), eventstore.AggregateType("org"), "asdf42", database.TextArray[string]{}, uint64(5)}, ), }, res: res{ diff --git a/internal/eventstore/v3/push.go b/internal/eventstore/v3/push.go index f09c3de515..47a4c96dca 100644 --- a/internal/eventstore/v3/push.go +++ b/internal/eventstore/v3/push.go @@ -13,11 +13,15 @@ import ( "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) +var appNamePrefix = dialect.DBPurposeEventPusher.AppName() + "_" + func (es *Eventstore) Push(ctx context.Context, commands ...eventstore.Command) (events []eventstore.Event, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -31,6 +35,20 @@ func (es *Eventstore) Push(ctx context.Context, commands ...eventstore.Command) sequences []*latestSequence ) + // needs to be set like this because psql complains about parameters in the SET statement + _, err = tx.ExecContext(ctx, "SET application_name = '"+appNamePrefix+authz.GetInstance(ctx).InstanceID()+"'") + if err != nil { + logging.WithError(err).Warn("failed to set application name") + return nil, err + } + + // needs to be set like this because psql complains about parameters in the SET statement + _, err = tx.ExecContext(ctx, "SET application_name = '"+appNamePrefix+authz.GetInstance(ctx).InstanceID()+"'") + if err != nil { + logging.WithError(err).Warn("failed to set application name") + return nil, err + } + err = crdb.ExecuteInTx(ctx, &transaction{tx}, func() (err error) { inTxCtx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() From 65e24b67dae6d9ab6eae600f8fd6fa0e794f0364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 19 Nov 2024 10:53:07 +0200 Subject: [PATCH 15/32] chore(load-test): disable userinfo after JWT profile (#8927) # Which Problems Are Solved Load-test requires single endpoint to be used for each test type. # How the Problems Are Solved Remove userinfo call from machine tests. # Additional Changes - Add load-test/.env to gitignore. # Additional Context - Related to #4424 --- load-test/.gitignore | 2 ++ load-test/src/use_cases/machine_jwt_profile_grant.ts | 3 --- .../src/use_cases/machine_jwt_profile_grant_single_user.ts | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) create mode 100644 load-test/.gitignore diff --git a/load-test/.gitignore b/load-test/.gitignore new file mode 100644 index 0000000000..30bd623c23 --- /dev/null +++ b/load-test/.gitignore @@ -0,0 +1,2 @@ +.env + diff --git a/load-test/src/use_cases/machine_jwt_profile_grant.ts b/load-test/src/use_cases/machine_jwt_profile_grant.ts index 084ac4f684..2511f9e2a5 100644 --- a/load-test/src/use_cases/machine_jwt_profile_grant.ts +++ b/load-test/src/use_cases/machine_jwt_profile_grant.ts @@ -46,9 +46,6 @@ export async function setup() { export default function (data: any) { token(new JWTProfileRequest(data.machines[__VU - 1].userId, data.machines[__VU - 1].keyId)) - .then((token) => { - userinfo(token.accessToken!) - }) } export function teardown(data: any) { diff --git a/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts b/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts index c654fb9492..95437b0c97 100644 --- a/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts +++ b/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts @@ -24,9 +24,6 @@ export async function setup() { export default function (data: any) { token(new JWTProfileRequest(data.machine.userId, data.machine.keyId)) - .then((token) => { - userinfo(token.accessToken!) - }) } export function teardown(data: any) { From c31b5df73b5c7e4161750360f2bc1996eccf075f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 19 Nov 2024 11:56:10 +0200 Subject: [PATCH 16/32] docs: add caches documentation (#8902) # Which Problems Are Solved Explain the usage of the new cache mechanisms. # How the Problems Are Solved Provide a dedicated page on caches with reference to `defaults.yaml`. # Additional Changes - Fix a broken link tag in token exchange docs. # Additional Context - Closes #8855 --- docs/docs/guides/integrate/token-exchange.mdx | 2 +- docs/docs/self-hosting/manage/cache.md | 233 ++++++++++++++++++ docs/sidebars.js | 1 + 3 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 docs/docs/self-hosting/manage/cache.md diff --git a/docs/docs/guides/integrate/token-exchange.mdx b/docs/docs/guides/integrate/token-exchange.mdx index ab3ee26f48..00fda4f17b 100644 --- a/docs/docs/guides/integrate/token-exchange.mdx +++ b/docs/docs/guides/integrate/token-exchange.mdx @@ -10,7 +10,7 @@ import TokenExchangeResponse from "../../apis/openidoauth/_token_exchange_respon The Token Exchange grant implements [RFC 8693, OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) and can be used to exchange tokens to a different scope, audience or subject. Changing the subject of an authenticated token is called impersonation or delegation. This guide will explain how token exchange is implemented inside ZITADEL and gives some usage examples. :::info -Token Exchange is currently an experimental beta](/docs/support/software-release-cycles-support#beta) feature. Be sure to enable it on the [feature API](#feature-api) before using it. +Token Exchange is currently an [experimental beta](/docs/support/software-release-cycles-support#beta) feature. Be sure to enable it on the [feature API](#feature-api) before using it. ::: In this guide we assume that the application performing the token exchange is already in possession of tokens. You should already have a good understanding on the following topics before starting with this guide: diff --git a/docs/docs/self-hosting/manage/cache.md b/docs/docs/self-hosting/manage/cache.md new file mode 100644 index 0000000000..2de0b43faa --- /dev/null +++ b/docs/docs/self-hosting/manage/cache.md @@ -0,0 +1,233 @@ +--- +title: Caches +sidebar_label: Caches +--- + +ZITADEL supports the use of a caches to speed up the lookup of frequently needed objects. As opposed to HTTP caches which might reside between ZITADEL and end-user applications, the cache build into ZITADEL uses active invalidation when an object gets updated. Another difference is that HTTP caches only cache the result of a complete request and the built-in cache stores objects needed for the internal business logic. For example, each request made to ZITADEL needs to retrieve and set [instance](/docs/concepts/structure/instance) information in middleware. + +:::info +Caches is currently an [experimental beta](/docs/support/software-release-cycles-support#beta) feature. +::: + +## Configuration + +The `Caches` configuration entry defines *connectors* which can be used by several objects. It is possible to mix *connectors* with different objects based on operational needs. + +```yaml +Caches: + Connectors: + SomeConnector: + Enabled: true + SomeOption: foo + SomeObject: + # Connector must be enabled above. + # When connector is empty, this cache will be disabled. + Connector: "SomeConnector" + MaxAge: 1h + LastUsage: 10m + # Log enables cache-specific logging. Default to error log to stderr when omitted. + Log: + Level: error +``` + +For a full configuration reference, please see the [runtime configuration file](/docs/self-hosting/manage/configure#runtime-configuration-file) section's `defaults.yaml`. + +## Connectors + +ZITADEL supports a number of *connectors*. Connectors integrate a cache with a storage backend. Users can combine connectors with the type of object cache depending on their operational and performance requirements. +When no connector is specified for an object cache, then no caching is performed. This is the current default. + +### Auto prune + +Some connectors take an `AutoPrune` option. This is provided for caches which don't have built-in expiry and cleanup routines. The auto pruner is a routine launched by ZITADEL and scans and removes outdated objects in the cache. Pruning can take a cost as they typically involve some kind of scan. However, using a long interval can cause higher storage utilization. + +```yaml +Caches: + Connectors: + Memory: + Enabled: true + # AutoPrune removes invalidated or expired object from the cache. + AutoPrune: + Interval: 1m + TimeOut: 5s +``` + +### Redis cache + +Redis is supported in simple mode. Cluster and Sentinel are not yet supported. There is also a circuit-breaker provided which prevents a single point of failure, should the single Redis instance become unavailable. + +Benefits: + +- Centralized cache with single source of truth +- Consistent invalidation +- Very fast when network latency is kept to a minimum +- Built-in object expiry, no pruner required + +Drawbacks: + +- Increased operational overhead: need to run a Redis instance as part of your infrastructure. +- When running multiple servers of ZITADEL in different regions, network roundtrip time might impact performance, neutralizing the benefit of a cache. + +#### Circuit breaker + +A [circuit breaker](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/dn589784(v=pandp.10)?redirectedfrom=MSDN) is provided for the Redis connector, to prevent a single point of failure in the case persistent errors. When the circuit breaker opens, the cache is temporary disabled and ignored. ZITADEL will continue to operate using queries to the database. + +```yaml +Caches: + Connectors: + Redis: + Enabled: true + Addr: localhost:6379 + # Many other options... + CircuitBreaker: + # Interval when the counters are reset to 0. + # 0 interval never resets the counters until the CB is opened. + Interval: 0 + # Amount of consecutive failures permitted + MaxConsecutiveFailures: 5 + # The ratio of failed requests out of total requests + MaxFailureRatio: 0.1 + # Timeout after opening of the CB, until the state is set to half-open. + Timeout: 60s + # The allowed amount of requests that are allowed to pass when the CB is half-open. + MaxRetryRequests: 1 +``` + +### PostgreSQL cache + +PostgreSQL can be used to store objects in unlogged tables. [Unlogged tables](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED) do not write to the WAL log and are therefore faster than regular tables. If the PostgreSQL server crashes, the data from those tables are lost. ZITADEL always creates the cache schema in the `zitadel` database during [setup](./updating_scaling#the-setup-phase). This connector requires a [pruner](#auto-prune) routine. + +Benefits: + +- Centralized cache with single source of truth +- No operational overhead. Reuses the query connection pool and the existing `zitadel` database. +- Consistent invalidation +- Faster than regular queries which often contain `JOIN` clauses. + +Drawbacks: + +- Slowest of the available caching options +- Might put additional strain on the database server, limiting horizontal scalability +- CockroachDB does not support unlogged tables. When this connector is enabled against CockroachDB, it does work but little to no performance benefit is to be expected. + +### Local memory cache + +ZITADEL is capable of caching object in local application memory, using hash-maps. Each ZITADEL server manages its own copy of the cache. This connector requires a [pruner](#auto-prune) routine. + +Benefits: + +- Fastest of the available caching options +- No operational overhead + +Drawbacks: + +- Inconsistent invalidation. An object validated in one ZITADEL server will not get invalidated in other servers. +- There's no single source of truth. Different servers may operate on a different version of an object +- Data is duplicated in each server, consuming more total memory inside a deployment. + +The drawbacks restricts its usefulness in distributed deployments. However simple installations running a single server can benefit greatly from this type of cache. For example test, development or home deployments. +If inconsistency is acceptable for short periods of time, one can choose to use this type of cache in distributed deployments with short max age configuration. + +**For example**: A ZITADEL deployment with 2 servers is serving 1000 req/sec total. The installation only has one instance[^1]. There is only a small amount of data cached (a few kB) so duplication is not a problem in this case. It is acceptable for [instance level setting](/docs/guides/manage/console/default-settings) to be out-dated for a short amount of time. When the memory cache is enabled for the instance objects, with a max age of 1 second, the instance only needs to be obtained from the database 2 times per second (once for each server). Saving 998 of redundant queries. Once an instance level setting is changed, it takes up to 1 second for all the servers to get the new state. + +## Objects + +The following section describes the type of objects ZITADEL can currently cache. Objects are actively invalidated at the cache backend when one of their properties is changed. Each object cache defines: + +- `Connector`: Selects the used [connector](#connectors) back-end. Must be activated first. +- `MaxAge`: the amount of time that an object is considered valid. When this age is passed the object is ignored (cache miss) and possibly cleaned up by the [pruner](#auto-prune) or other built-in garbage collection. +- `LastUsage`: defines usage based lifetime. Each time an object is used, its usage timestamp is updated. Popular objects remain cached, while unused objects are cleaned up. This option can be used to indirectly limit the size of the cache. +- `Log`: allows specific log settings for the cache. This can be used to debug a certain cache without having to change the global log level. + +```yaml +Caches: + SomeObject: + # Connector must be enabled above. + # When connector is empty, this cache will be disabled. + Connector: "" + MaxAge: 1h + LastUsage: 10m + # Log enables cache-specific logging. Default to error log to stderr when omitted. + Log: + Level: error + AddSource: true + Formatter: + Format: text +``` + +### Instance + +All HTTP and gRPC requests sent to ZITADEL receive an instance context. The instance is usually resolved by the domain from the request. In some cases, like the [system service](/docs/apis/resources/system/system-service), the instance can be resolved by its ID. An instance object contains many of the [default settings](/docs/guides/manage/console/default-settings): + +- Instance [features](/docs/guides/manage/console/default-settings#features) +- Instance domains: generated and [custom](/docs/guides/manage/cloud/instances#add-custom-domain) +- [Trusted domains](/docs/apis/resources/admin/admin-service-add-instance-trusted-domain) +- Security settings ([IFrame policy](/docs/guides/solution-scenarios/configurations#embedding-zitadel-in-an-iframe)) +- Limits[^2] +- [Allowed languages](/docs/guides/manage/console/default-settings#languages) + +These settings typically change infrequently in production. ***Every*** request made to ZITADEL needs to query for the instance. This is a typical case of set once, get many times where a cache can provide a significant optimization. + +### Milestones + +Milestones are used to track the administrator's progress in setting up their instance. Milestones are used to render *your next steps* in the [console](/docs/guides/manage/console/overview) landing page. +Milestones are reached upon the first time a certain action is performed. For example the first application created or the first human login. In order to push a "reached" event only once, ZITADEL must keep track of the current state of milestones by an eventstore query every time an eligible action is performed. This can cause an unwanted overhead on production servers, therefore they are cached. + +As an extra optimization, once all milestones are reached by the instance, an in-memory flag is set and the milestone state is never queried again from the database nor cache. +For single instance setups which fulfilled all milestone (*your next steps* in console) it is not needed to enable this cache. We mainly use it for ZITADEL cloud where there are many instances with *incomplete* milestones. + +## Examples + +Currently caches are in beta and disabled by default. However, if you want to give caching a try, the following sections contains some suggested configurations for different setups. + +The following configuration is recommended for single instance setups with a single ZITADEL server: + +```yaml +Caches: + Memory: + Enabled: true + Instance: + Connector: "memory" + MaxAge: 1h +``` + +The following configuration is recommended for single instance setups with high traffic on multiple servers, where Redis is not available: + +```yaml +Caches: + Memory: + Enabled: true + Postgres: + Enabled: true + Instance: + Connector: "memory" + MaxAge: 1s + Milestones: + Connector: "postgres" + MaxAge: 1h + LastUsage: 10m +``` + +When running many instances on multiple servers: + +```yaml +Caches: + Connectors: + Redis: + Enabled: true + # Other connection options + + Instance: + Connector: "redis" + MaxAge: 1h + LastUsage: 10m + Milestones: + Connector: "redis" + MaxAge: 1h + LastUsage: 10m +``` +---- + +[^1]: Many deployments of ZITADEL have only one or few [instances](/docs/concepts/structure/instance). Multiple instances are mostly used for ZITADEL cloud, where each customer gets at least one instance. + +[^2]: Limits are imposed by the system API, usually when customers exceed their subscription in ZITADEL cloud. \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index be96b61706..ed75894399 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -913,6 +913,7 @@ module.exports = { "self-hosting/manage/http2", "self-hosting/manage/tls_modes", "self-hosting/manage/database/database", + "self-hosting/manage/cache", "self-hosting/manage/updating_scaling", "self-hosting/manage/usage_control", { From 041c3d9b9e41594c2c05a55739ba50decf06e72f Mon Sep 17 00:00:00 2001 From: zitadelraccine Date: Tue, 19 Nov 2024 07:00:58 -0500 Subject: [PATCH 17/32] docs: add next office hours (#8922) # Which Problems Are Solved Updating the meeting schedule with the latest community event. # How the Problems Are Solved A new event invite with associated details are added to direct community members on Github to register for our Discord event. # Additional Changes N/A # Additional Context N/A Co-authored-by: Silvan --- MEETING_SCHEDULE.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/MEETING_SCHEDULE.md b/MEETING_SCHEDULE.md index 695a19855f..8d59970611 100644 --- a/MEETING_SCHEDULE.md +++ b/MEETING_SCHEDULE.md @@ -3,6 +3,35 @@ Dear community! We're excited to announce bi-weekly office hours. +## #6 Q&A + +Hey folks! + +We’re inviting you to our next open office hours session! C: From leveraging ZITADEL actions to exploring your use cases, join our hosts Silvan & Stefan on Wednesday, November 20, 2024 at 11:00 AM (EST) as they answer your questions about ZITADEL! + +🦒 **What to expect** + +An open Q&A session - Share your questions and support others with their inquiries. +A space to share your thoughts / feedback on the ZITADEL platform + +🗒️ **Details** + +Target audience: All ZITADEL platform users & community members +Topic: Q&A Session +Date & time: Wednesday, November 20, 2024 at 11:00 AM (EST) +Duration: ~1 hour +Platform: ZITADEL’s Discord stage channel + +Register for this event here ➡️ https://discord.gg/bnuAe2RX?event=1307010383713927230 + +🗓️ **Add this to your calendar** ➡️ [Google Calendar](https://calendar.google.com/calendar/u/0/r/eventedit?dates=20241120T110000/20241120T110000&details=We%E2%80%99re+inviting+you+to+our+next+open+office+hours+session!+C:+From+leveraging+ZITADEL+actions+to+exploring+your+use+cases,+join+our+hosts+Silvan+%26+Stefan+as+they+answer+your+questions+about+ZITADEL!+%0A%0A**What+to+expect**%0A%0A-+An+open+Q%26A+session+-+Share+your+questions+and+support+others+with+their+inquiries.+%0A-+A+space+to+share+your+thoughts+/+feedback+on+the+ZITADEL+platform+++%0A%0A**Details**+%0A%0A**Target+audience:**+All+ZITADEL+platform+users+%26+community+members%0A**Topic**:+Q%26A+Session+%0A**Date+%26+time**:+Wednesday,+November+20,+2024+11:00+AM%0A**Duration**:+~1+hour+%0A**Platform**:+ZITADEL%E2%80%99s+Discord+stage+channel&location=Discord:+ZITADEL+server,+office+hours&text=Open+Office+Hours) + + +If you have any questions prior to the live session, be sure to share them in the office hours stage chat + +Looking forward to seeing you there! Share this with other ZITADEL users & people who might be interested in ZITADEL! It’s appreciated 🫶 + + ## #5 Q&A Dear community, From c165ed07f41e64fdfa6360c7aa937eadf22a5fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 21 Nov 2024 08:05:03 +0200 Subject: [PATCH 18/32] feat(cache): organization (#8903) # Which Problems Are Solved Organizations are ofter searched for by ID or primary domain. This results in many redundant queries, resulting in a performance impact. # How the Problems Are Solved Cache Organizaion objects by ID and primary domain. # Additional Changes - Adjust integration test config to use all types of cache. - Adjust integration test lifetimes so the pruner has something to do while the tests run. # Additional Context - Closes #8865 - After #8902 --- cmd/defaults.yaml | 10 +++ docs/docs/self-hosting/manage/cache.md | 20 ++++++ internal/cache/cache.go | 1 + internal/cache/connector/connector.go | 5 +- internal/cache/purpose_enumer.go | 12 ++-- internal/integration/config/zitadel.yaml | 24 +++---- internal/query/cache.go | 7 ++ internal/query/instance.go | 4 +- internal/query/org.go | 46 ++++++++++++- internal/query/orgindex_enumer.go | 82 ++++++++++++++++++++++++ 10 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 internal/query/orgindex_enumer.go diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 4854356455..f0c8bedbeb 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -328,6 +328,16 @@ Caches: AddSource: true Formatter: Format: text + # Organization cache, gettable by primary domain or ID. + Organization: + Connector: "" + MaxAge: 1h + LastUsage: 10m + Log: + Level: error + AddSource: true + Formatter: + Format: text Machine: # Cloud-hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified. diff --git a/docs/docs/self-hosting/manage/cache.md b/docs/docs/self-hosting/manage/cache.md index 2de0b43faa..def2ece633 100644 --- a/docs/docs/self-hosting/manage/cache.md +++ b/docs/docs/self-hosting/manage/cache.md @@ -176,6 +176,16 @@ Milestones are reached upon the first time a certain action is performed. For ex As an extra optimization, once all milestones are reached by the instance, an in-memory flag is set and the milestone state is never queried again from the database nor cache. For single instance setups which fulfilled all milestone (*your next steps* in console) it is not needed to enable this cache. We mainly use it for ZITADEL cloud where there are many instances with *incomplete* milestones. +### Organization + +Most resources like users, project and applications are part of an [organization](/docs/concepts/structure/organizations). Therefore many parts of the ZITADEL logic search for an organization by ID or by their primary domain. +Organization objects are quite small and receive infrequent updates after they are created: + +- Change of organization name +- Deactivation / Reactivation +- Change of primary domain +- Removal + ## Examples Currently caches are in beta and disabled by default. However, if you want to give caching a try, the following sections contains some suggested configurations for different setups. @@ -189,6 +199,9 @@ Caches: Instance: Connector: "memory" MaxAge: 1h + Organization: + Connector: "memory" + MaxAge: 1h ``` The following configuration is recommended for single instance setups with high traffic on multiple servers, where Redis is not available: @@ -206,6 +219,9 @@ Caches: Connector: "postgres" MaxAge: 1h LastUsage: 10m + Organization: + Connector: "memory" + MaxAge: 1s ``` When running many instances on multiple servers: @@ -225,6 +241,10 @@ Caches: Connector: "redis" MaxAge: 1h LastUsage: 10m + Organization: + Connector: "redis" + MaxAge: 1h + LastUsage: 10m ``` ---- diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 9e92f50988..c7dbad6f2c 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -16,6 +16,7 @@ const ( PurposeUnspecified Purpose = iota PurposeAuthzInstance PurposeMilestones + PurposeOrganization ) // Cache stores objects with a value of type `V`. diff --git a/internal/cache/connector/connector.go b/internal/cache/connector/connector.go index 0c4fb9ccc6..09298fa688 100644 --- a/internal/cache/connector/connector.go +++ b/internal/cache/connector/connector.go @@ -19,8 +19,9 @@ type CachesConfig struct { Postgres pg.Config Redis redis.Config } - Instance *cache.Config - Milestones *cache.Config + Instance *cache.Config + Milestones *cache.Config + Organization *cache.Config } type Connectors struct { diff --git a/internal/cache/purpose_enumer.go b/internal/cache/purpose_enumer.go index bae47476ff..47ad167d70 100644 --- a/internal/cache/purpose_enumer.go +++ b/internal/cache/purpose_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _PurposeName = "unspecifiedauthz_instancemilestones" +const _PurposeName = "unspecifiedauthz_instancemilestonesorganization" -var _PurposeIndex = [...]uint8{0, 11, 25, 35} +var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47} -const _PurposeLowerName = "unspecifiedauthz_instancemilestones" +const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganization" func (i Purpose) String() string { if i < 0 || i >= Purpose(len(_PurposeIndex)-1) { @@ -27,9 +27,10 @@ func _PurposeNoOp() { _ = x[PurposeUnspecified-(0)] _ = x[PurposeAuthzInstance-(1)] _ = x[PurposeMilestones-(2)] + _ = x[PurposeOrganization-(3)] } -var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones} +var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization} var _PurposeNameToValueMap = map[string]Purpose{ _PurposeName[0:11]: PurposeUnspecified, @@ -38,12 +39,15 @@ var _PurposeNameToValueMap = map[string]Purpose{ _PurposeLowerName[11:25]: PurposeAuthzInstance, _PurposeName[25:35]: PurposeMilestones, _PurposeLowerName[25:35]: PurposeMilestones, + _PurposeName[35:47]: PurposeOrganization, + _PurposeLowerName[35:47]: PurposeOrganization, } var _PurposeNames = []string{ _PurposeName[0:11], _PurposeName[11:25], _PurposeName[25:35], + _PurposeName[35:47], } // PurposeString retrieves an enum value from the enum constants string name. diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index 378dc2f09b..e2642d9b8f 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -8,28 +8,30 @@ TLS: Caches: Connectors: + Memory: + Enabled: true Postgres: Enabled: true Redis: Enabled: true Instance: - Connector: "redis" - MaxAge: 1h - LastUsage: 10m + Connector: "memory" + MaxAge: 5m + LastUsage: 1m Log: Level: info - AddSource: true - Formatter: - Format: text Milestones: Connector: "postgres" - MaxAge: 1h - LastUsage: 10m + MaxAge: 5m + LastUsage: 1m + Log: + Level: info + Organization: + Connector: "redis" + MaxAge: 5m + LastUsage: 1m Log: Level: info - AddSource: true - Formatter: - Format: text Quotas: Access: diff --git a/internal/query/cache.go b/internal/query/cache.go index 55f7bb3db6..949e121c1f 100644 --- a/internal/query/cache.go +++ b/internal/query/cache.go @@ -12,6 +12,7 @@ import ( type Caches struct { instance cache.Cache[instanceIndex, string, *authzInstance] + org cache.Cache[orgIndex, string, *Org] } func startCaches(background context.Context, connectors connector.Connectors) (_ *Caches, err error) { @@ -20,7 +21,13 @@ func startCaches(background context.Context, connectors connector.Connectors) (_ if err != nil { return nil, err } + caches.org, err = connector.StartCache[orgIndex, string, *Org](background, orgIndexValues(), cache.PurposeOrganization, connectors.Config.Organization, connectors) + if err != nil { + return nil, err + } + caches.registerInstanceInvalidation() + caches.registerOrgInvalidation() return caches, nil } diff --git a/internal/query/instance.go b/internal/query/instance.go index 549c05a233..8dd0db7d89 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -522,9 +522,9 @@ func (i *authzInstance) Keys(index instanceIndex) []string { return []string{i.ID} case instanceIndexByHost: return i.ExternalDomains - default: - return nil + case instanceIndexUnspecified: } + return nil } func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) { diff --git a/internal/query/org.go b/internal/query/org.go index 1c20255171..a57867d92b 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -107,10 +107,19 @@ func (q *OrgSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query } -func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string) (_ *Org, err error) { +func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string) (org *Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + if org, ok := q.caches.org.Get(ctx, orgIndexByID, id); ok { + return org, nil + } + defer func() { + if err == nil && org != nil { + q.caches.org.Set(ctx, org) + } + }() + if !authz.GetInstance(ctx).Features().ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeOrgByID) { return q.oldOrgByID(ctx, shouldTriggerBulk, id) } @@ -175,6 +184,11 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, domain) + if ok { + return org, nil + } + stmt, scan := prepareOrgQuery(ctx, q.client) query, args, err := stmt.Where(sq.Eq{ OrgColumnDomain.identifier(): domain, @@ -189,6 +203,9 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O org, err = scan(row) return err }, query, args...) + if err == nil { + q.caches.org.Set(ctx, org) + } return org, err } @@ -476,3 +493,30 @@ func prepareOrgUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu return isUnique, err } } + +type orgIndex int + +//go:generate enumer -type orgIndex -linecomment +const ( + // Empty line comment ensures empty string for unspecified value + orgIndexUnspecified orgIndex = iota // + orgIndexByID + orgIndexByPrimaryDomain +) + +// Keys implements [cache.Entry] +func (o *Org) Keys(index orgIndex) []string { + switch index { + case orgIndexByID: + return []string{o.ID} + case orgIndexByPrimaryDomain: + return []string{o.Domain} + case orgIndexUnspecified: + } + return nil +} + +func (c *Caches) registerOrgInvalidation() { + invalidate := cacheInvalidationFunc(c.instance, instanceIndexByID, getAggregateID) + projection.OrgProjection.RegisterCacheInvalidation(invalidate) +} diff --git a/internal/query/orgindex_enumer.go b/internal/query/orgindex_enumer.go new file mode 100644 index 0000000000..74f7c985c9 --- /dev/null +++ b/internal/query/orgindex_enumer.go @@ -0,0 +1,82 @@ +// Code generated by "enumer -type orgIndex -linecomment"; DO NOT EDIT. + +package query + +import ( + "fmt" + "strings" +) + +const _orgIndexName = "orgIndexByIDorgIndexByPrimaryDomain" + +var _orgIndexIndex = [...]uint8{0, 0, 12, 35} + +const _orgIndexLowerName = "orgindexbyidorgindexbyprimarydomain" + +func (i orgIndex) String() string { + if i < 0 || i >= orgIndex(len(_orgIndexIndex)-1) { + return fmt.Sprintf("orgIndex(%d)", i) + } + return _orgIndexName[_orgIndexIndex[i]:_orgIndexIndex[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 _orgIndexNoOp() { + var x [1]struct{} + _ = x[orgIndexUnspecified-(0)] + _ = x[orgIndexByID-(1)] + _ = x[orgIndexByPrimaryDomain-(2)] +} + +var _orgIndexValues = []orgIndex{orgIndexUnspecified, orgIndexByID, orgIndexByPrimaryDomain} + +var _orgIndexNameToValueMap = map[string]orgIndex{ + _orgIndexName[0:0]: orgIndexUnspecified, + _orgIndexLowerName[0:0]: orgIndexUnspecified, + _orgIndexName[0:12]: orgIndexByID, + _orgIndexLowerName[0:12]: orgIndexByID, + _orgIndexName[12:35]: orgIndexByPrimaryDomain, + _orgIndexLowerName[12:35]: orgIndexByPrimaryDomain, +} + +var _orgIndexNames = []string{ + _orgIndexName[0:0], + _orgIndexName[0:12], + _orgIndexName[12:35], +} + +// orgIndexString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func orgIndexString(s string) (orgIndex, error) { + if val, ok := _orgIndexNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _orgIndexNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to orgIndex values", s) +} + +// orgIndexValues returns all values of the enum +func orgIndexValues() []orgIndex { + return _orgIndexValues +} + +// orgIndexStrings returns a slice of all String values of the enum +func orgIndexStrings() []string { + strs := make([]string, len(_orgIndexNames)) + copy(strs, _orgIndexNames) + return strs +} + +// IsAorgIndex returns "true" if the value is listed in the enum definition. "false" otherwise +func (i orgIndex) IsAorgIndex() bool { + for _, v := range _orgIndexValues { + if i == v { + return true + } + } + return false +} From b65266907c0102beb10fb49ceee6999d7128023d Mon Sep 17 00:00:00 2001 From: Titouan-joseph Cicorella Date: Thu, 21 Nov 2024 14:26:39 +0100 Subject: [PATCH 19/32] docs(user): change some deprecated links (#8913) # Which Problems Are Solved Some links are pointing to the deprecated API v1 # How the Problems Are Solved Change the link to the API V2 # Additional Changes For the moment, I don't have the time to add more links in the API v1 pages. Maybe later, when I will have time, I will add more links --------- Co-authored-by: Fabi --- docs/docs/guides/manage/user/reg-create-user.md | 2 +- proto/zitadel/management.proto | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/guides/manage/user/reg-create-user.md b/docs/docs/guides/manage/user/reg-create-user.md index 0ac85e3b33..9c523cba24 100644 --- a/docs/docs/guides/manage/user/reg-create-user.md +++ b/docs/docs/guides/manage/user/reg-create-user.md @@ -6,7 +6,7 @@ The ZITADEL API has different possibilities to create users. This can be used, if you are building your own registration page. Use the following API call to create your users: -[Create User (Human)](/apis/resources/mgmt/management-service-import-human-user.api.mdx) +[Create User (Human)](apis/resources/user_service_v2/user-service-add-human-user.api.mdx) ## With Username and Password diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 0df07ffd4c..0ff2ad7b75 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -472,7 +472,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Create/Import User (Human)"; - description: "Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: please use user service v2 AddHumanUser" + description: "Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: please use user service v2 [AddHumanUser](apis/resources/user_service_v2/user-service-add-human-user.api.mdx)" tags: "Users"; tags: "User Human" deprecated: true; From d4389ab359e76f745185be7c36ddde616a35472a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 21 Nov 2024 16:46:30 +0200 Subject: [PATCH 20/32] feat(eventstore): add row locking option (#8939) # Which Problems Are Solved We need a reliable way to lock events that are being processed as part of a job queue. For example in the notification handlers. # How the Problems Are Solved Allow setting `FOR UPDATE [ NOWAIT | SKIP LOCKED ]` to the eventstore query builder using an open transaction. - NOWAIT returns an errors if the lock cannot be obtained - SKIP LOCKED only returns row which are not locked. - Default is to wait for the lock to be released. # Additional Changes - none # Additional Context - [Locking docs](https://www.postgresql.org/docs/17/sql-select.html#SQL-FOR-UPDATE-SHARE) - Related to https://github.com/zitadel/zitadel/issues/8931 --- .../eventstore/repository/search_query.go | 3 + internal/eventstore/repository/sql/query.go | 12 ++ .../eventstore/repository/sql/query_test.go | 146 ++++++++++++++++++ internal/eventstore/search_query.go | 27 ++++ 4 files changed, 188 insertions(+) diff --git a/internal/eventstore/repository/search_query.go b/internal/eventstore/repository/search_query.go index e67ed6206c..32a353049d 100644 --- a/internal/eventstore/repository/search_query.go +++ b/internal/eventstore/repository/search_query.go @@ -14,6 +14,8 @@ type SearchQuery struct { SubQueries [][]*Filter Tx *sql.Tx + LockRows bool + LockOption eventstore.LockOption AllowTimeTravel bool AwaitOpenTransactions bool Limit uint64 @@ -130,6 +132,7 @@ func QueryFromBuilder(builder *eventstore.SearchQueryBuilder) (*SearchQuery, err AwaitOpenTransactions: builder.GetAwaitOpenTransactions(), SubQueries: make([][]*Filter, len(builder.GetQueries())), } + query.LockRows, query.LockOption = builder.GetLockRows() for _, f := range []func(builder *eventstore.SearchQueryBuilder, query *SearchQuery) *Filter{ instanceIDFilter, diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index 8ecea6e3d6..dda12187f9 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -105,6 +105,18 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search query += " OFFSET ?" } + if q.LockRows { + query += " FOR UPDATE" + switch q.LockOption { + case eventstore.LockOptionWait: // default behavior + case eventstore.LockOptionNoWait: + query += " NOWAIT" + case eventstore.LockOptionSkipLocked: + query += " SKIP LOCKED" + + } + } + query = criteria.placeholder(query) var contextQuerier interface { diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 2956c39bd5..a3d47b838d 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -657,6 +657,89 @@ func Test_query_events_with_crdb(t *testing.T) { } } +/* Cockroach test DB doesn't seem to lock +func Test_query_events_with_crdb_locking(t *testing.T) { + type args struct { + searchQuery *eventstore.SearchQueryBuilder + } + type fields struct { + existingEvents []eventstore.Command + client *sql.DB + } + tests := []struct { + name string + fields fields + args args + lockOption eventstore.LockOption + wantErr bool + }{ + { + name: "skip locked", + fields: fields{ + client: testCRDBClient, + existingEvents: []eventstore.Command{ + generateEvent(t, "306", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), + generateEvent(t, "307", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), + generateEvent(t, "308", func(e *repository.Event) { e.ResourceOwner = sql.NullString{String: "caos", Valid: true} }), + }, + }, + args: args{ + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner("caos"), + }, + lockOption: eventstore.LockOptionNoWait, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db := &CRDB{ + DB: &database.DB{ + DB: tt.fields.client, + Database: new(testDB), + }, + } + // setup initial data for query + if _, err := db.Push(context.Background(), tt.fields.existingEvents...); err != nil { + t.Errorf("error in setup = %v", err) + return + } + // first TX should lock and return all events + tx1, err := db.DB.Begin() + require.NoError(t, err) + defer func() { + require.NoError(t, tx1.Rollback()) + }() + searchQuery1 := tt.args.searchQuery.LockRowsDuringTx(tx1, tt.lockOption) + gotEvents1 := []eventstore.Event{} + err = query(context.Background(), db, searchQuery1, eventstore.Reducer(func(event eventstore.Event) error { + gotEvents1 = append(gotEvents1, event) + return nil + }), true) + require.NoError(t, err) + assert.Len(t, gotEvents1, len(tt.fields.existingEvents)) + + // second TX should not return the events, and might return an error + tx2, err := db.DB.Begin() + require.NoError(t, err) + defer func() { + require.NoError(t, tx2.Rollback()) + }() + searchQuery2 := tt.args.searchQuery.LockRowsDuringTx(tx1, tt.lockOption) + gotEvents2 := []eventstore.Event{} + err = query(context.Background(), db, searchQuery2, eventstore.Reducer(func(event eventstore.Event) error { + gotEvents2 = append(gotEvents2, event) + return nil + }), true) + if tt.wantErr { + require.Error(t, err) + } + require.NoError(t, err) + assert.Len(t, gotEvents2, 0) + }) + } +} +*/ + func Test_query_events_mocked(t *testing.T) { type args struct { query *eventstore.SearchQueryBuilder @@ -762,6 +845,69 @@ func Test_query_events_mocked(t *testing.T) { wantErr: false, }, }, + { + name: "lock, wait", + args: args{ + dest: &[]*repository.Event{}, + query: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + OrderDesc(). + Limit(5). + AddQuery(). + AggregateTypes("user"). + Builder().LockRowsDuringTx(nil, eventstore.LockOptionWait), + }, + fields: fields{ + mock: newMockClient(t).expectQuery(t, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE`, + []driver.Value{eventstore.AggregateType("user"), uint64(5)}, + ), + }, + res: res{ + wantErr: false, + }, + }, + { + name: "lock, no wait", + args: args{ + dest: &[]*repository.Event{}, + query: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + OrderDesc(). + Limit(5). + AddQuery(). + AggregateTypes("user"). + Builder().LockRowsDuringTx(nil, eventstore.LockOptionNoWait), + }, + fields: fields{ + mock: newMockClient(t).expectQuery(t, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE NOWAIT`, + []driver.Value{eventstore.AggregateType("user"), uint64(5)}, + ), + }, + res: res{ + wantErr: false, + }, + }, + { + name: "lock, skip locked", + args: args{ + dest: &[]*repository.Event{}, + query: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + OrderDesc(). + Limit(5). + AddQuery(). + AggregateTypes("user"). + Builder().LockRowsDuringTx(nil, eventstore.LockOptionSkipLocked), + }, + fields: fields{ + mock: newMockClient(t).expectQuery(t, + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = \$1 ORDER BY event_sequence DESC LIMIT \$2 FOR UPDATE SKIP LOCKED`, + []driver.Value{eventstore.AggregateType("user"), uint64(5)}, + ), + }, + res: res{ + wantErr: false, + }, + }, { name: "error sql conn closed", args: args{ diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index e80ed8295c..58e25f6567 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -22,6 +22,8 @@ type SearchQueryBuilder struct { editorUser string queries []*SearchQuery tx *sql.Tx + lockRows bool + lockOption LockOption allowTimeTravel bool positionAfter float64 awaitOpenTransactions bool @@ -94,6 +96,10 @@ func (q SearchQueryBuilder) GetCreationDateBefore() time.Time { return q.creationDateBefore } +func (q SearchQueryBuilder) GetLockRows() (bool, LockOption) { + return q.lockRows, q.lockOption +} + // ensureInstanceID makes sure that the instance id is always set func (b *SearchQueryBuilder) ensureInstanceID(ctx context.Context) { if b.instanceID == nil && len(b.instanceIDs) == 0 && authz.GetInstance(ctx).InstanceID() != "" { @@ -307,6 +313,27 @@ func (builder *SearchQueryBuilder) CreationDateBefore(creationDate time.Time) *S return builder } +type LockOption int + +const ( + // Wait until the previous lock on all of the selected rows is released (default) + LockOptionWait LockOption = iota + // With NOWAIT, the statement reports an error, rather than waiting, if a selected row cannot be locked immediately. + LockOptionNoWait + // With SKIP LOCKED, any selected rows that cannot be immediately locked are skipped. + LockOptionSkipLocked +) + +// LockRowsDuringTx locks the found rows for the duration of the transaction, +// using the [`FOR UPDATE`](https://www.postgresql.org/docs/17/sql-select.html#SQL-FOR-UPDATE-SHARE) lock strength. +// The lock is removed on transaction commit or rollback. +func (builder *SearchQueryBuilder) LockRowsDuringTx(tx *sql.Tx, option LockOption) *SearchQueryBuilder { + builder.tx = tx + builder.lockRows = true + builder.lockOption = option + return builder +} + // AddQuery creates a new sub query. // All fields in the sub query are AND-connected in the storage request. // Multiple sub queries are OR-connected in the storage request. From 48ffc902cc90237d693e7104fc742ee927478da7 Mon Sep 17 00:00:00 2001 From: Dominic Bachmann Date: Fri, 22 Nov 2024 10:25:25 +0100 Subject: [PATCH 21/32] fix: typo in defaults.yaml where ExternalPort should be ExternalDomain (#8923) # Which Problems Are Solved Fixed a typo in cmd/defaults.yaml where ExternalPort should be ExternalDomain Co-authored-by: Fabi --- cmd/defaults.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index f0c8bedbeb..e15d491a8b 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -68,7 +68,7 @@ Port: 8080 # ZITADEL_PORT # It can differ from Port e.g. if a reverse proxy forwards the traffic to ZITADEL # Read more about external access: https://zitadel.com/docs/self-hosting/manage/custom-domain ExternalPort: 8080 # ZITADEL_EXTERNALPORT -# ExternalPort is the domain on which end users access ZITADEL. +# ExternalDomain is the domain on which end users access ZITADEL. # Read more about external access: https://zitadel.com/docs/self-hosting/manage/custom-domain ExternalDomain: localhost # ZITADEL_EXTERNALDOMAIN # ExternalSecure specifies if ZITADEL is exposed externally using HTTPS or HTTP. From 1ee7a1ab7ca30438529ad33f4186c78b886aec0f Mon Sep 17 00:00:00 2001 From: Silvan Date: Fri, 22 Nov 2024 17:25:28 +0100 Subject: [PATCH 22/32] feat(eventstore): accept transaction in push (#8945) # Which Problems Are Solved Push is not capable of external transactions. # How the Problems Are Solved A new function `PushWithClient` is added to the eventstore framework which allows to pass a client which can either be a `*sql.Client` or `*sql.Tx` and is used during push. # Additional Changes Added interfaces to database package. # Additional Context - part of https://github.com/zitadel/zitadel/issues/8931 --------- Co-authored-by: Livio Spring Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../instance_debug_notification_log_test.go | 4 +- internal/database/database.go | 36 ++++++++ internal/eventstore/eventstore.go | 30 +++++-- internal/eventstore/eventstore_bench_test.go | 4 +- internal/eventstore/eventstore_pusher_test.go | 4 +- internal/eventstore/eventstore_test.go | 8 +- .../repository/mock/repository.mock.go | 24 +++-- .../repository/mock/repository.mock.impl.go | 17 ++-- internal/eventstore/v3/eventstore.go | 8 ++ internal/eventstore/v3/field.go | 15 ++-- internal/eventstore/v3/push.go | 90 +++++++++++-------- internal/eventstore/v3/sequence.go | 3 +- internal/eventstore/v3/unique_constraints.go | 4 +- 13 files changed, 177 insertions(+), 70 deletions(-) diff --git a/internal/command/instance_debug_notification_log_test.go b/internal/command/instance_debug_notification_log_test.go index 9190064f60..32fdd06618 100644 --- a/internal/command/instance_debug_notification_log_test.go +++ b/internal/command/instance_debug_notification_log_test.go @@ -199,7 +199,7 @@ func TestCommandSide_ChangeDebugNotificationProviderLog(t *testing.T) { }, }, { - name: "change, ok", + name: "change, ok 1", fields: fields{ eventstore: eventstoreExpect( t, @@ -232,7 +232,7 @@ func TestCommandSide_ChangeDebugNotificationProviderLog(t *testing.T) { }, }, { - name: "change, ok", + name: "change, ok 2", fields: fields{ eventstore: eventstoreExpect( t, diff --git a/internal/database/database.go b/internal/database/database.go index d6ccf2873c..0191f34b6d 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -18,6 +18,42 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +type QueryExecuter interface { + Query(query string, args ...any) (*sql.Rows, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + Exec(query string, args ...any) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) +} + +type Client interface { + QueryExecuter + BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) + Begin() (*sql.Tx, error) +} + +type Tx interface { + QueryExecuter + Commit() error + Rollback() error +} + +var ( + _ Client = (*sql.DB)(nil) + _ Tx = (*sql.Tx)(nil) +) + +func CloseTransaction(tx Tx, err error) error { + if err != nil { + rollbackErr := tx.Rollback() + logging.OnError(rollbackErr).Error("failed to rollback transaction") + return err + } + + commitErr := tx.Commit() + logging.OnError(commitErr).Error("failed to commit transaction") + return commitErr +} + type Config struct { Dialects map[string]interface{} `mapstructure:",remain"` EventPushConnRatio float64 diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index a8c8e923b5..6246978739 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -85,6 +85,12 @@ func (es *Eventstore) Health(ctx context.Context) error { // Push pushes the events in a single transaction // an event needs at least an aggregate func (es *Eventstore) Push(ctx context.Context, cmds ...Command) ([]Event, error) { + return es.PushWithClient(ctx, nil, cmds...) +} + +// PushWithClient pushes the events in a single transaction using the provided database client +// an event needs at least an aggregate +func (es *Eventstore) PushWithClient(ctx context.Context, client database.Client, cmds ...Command) ([]Event, error) { if es.PushTimeout > 0 { var cancel func() ctx, cancel = context.WithTimeout(ctx, es.PushTimeout) @@ -100,12 +106,24 @@ func (es *Eventstore) Push(ctx context.Context, cmds ...Command) ([]Event, error // https://github.com/zitadel/zitadel/issues/7202 retry: for i := 0; i <= es.maxRetries; i++ { - events, err = es.pusher.Push(ctx, cmds...) - var pgErr *pgconn.PgError - if !errors.As(err, &pgErr) || pgErr.ConstraintName != "events2_pkey" || pgErr.SQLState() != "23505" { + events, err = es.pusher.Push(ctx, client, cmds...) + // if there is a transaction passed the calling function needs to retry + if _, ok := client.(database.Tx); ok { break retry } - logging.WithError(err).Info("eventstore push retry") + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) { + break retry + } + if pgErr.ConstraintName == "events2_pkey" && pgErr.SQLState() == "23505" { + logging.WithError(err).Info("eventstore push retry") + continue + } + if pgErr.SQLState() == "CR000" || pgErr.SQLState() == "40001" { + logging.WithError(err).Info("eventstore push retry") + continue + } + break retry } if err != nil { return nil, err @@ -283,7 +301,9 @@ type Pusher interface { // Health checks if the connection to the storage is available Health(ctx context.Context) error // Push stores the actions - Push(ctx context.Context, commands ...Command) (_ []Event, err error) + Push(ctx context.Context, client database.QueryExecuter, commands ...Command) (_ []Event, err error) + // Client returns the underlying database connection + Client() *database.DB } type FillFieldsEvent interface { diff --git a/internal/eventstore/eventstore_bench_test.go b/internal/eventstore/eventstore_bench_test.go index 69b958abd8..582391e09f 100644 --- a/internal/eventstore/eventstore_bench_test.go +++ b/internal/eventstore/eventstore_bench_test.go @@ -69,7 +69,7 @@ func Benchmark_Push_SameAggregate(b *testing.B) { b.StartTimer() for n := 0; n < b.N; n++ { - _, err := store.Push(ctx, cmds...) + _, err := store.Push(ctx, store.Client().DB, cmds...) if err != nil { b.Error(err) } @@ -149,7 +149,7 @@ func Benchmark_Push_MultipleAggregate_Parallel(b *testing.B) { b.RunParallel(func(p *testing.PB) { for p.Next() { i++ - _, err := store.Push(ctx, commandCreator(strconv.Itoa(i))...) + _, err := store.Push(ctx, store.Client().DB, commandCreator(strconv.Itoa(i))...) if err != nil { b.Error(err) } diff --git a/internal/eventstore/eventstore_pusher_test.go b/internal/eventstore/eventstore_pusher_test.go index bd97b2e1e6..4e8e663667 100644 --- a/internal/eventstore/eventstore_pusher_test.go +++ b/internal/eventstore/eventstore_pusher_test.go @@ -607,7 +607,7 @@ func TestCRDB_Push_ResourceOwner(t *testing.T) { } } -func pushAggregates(pusher eventstore.Pusher, aggregateCommands [][]eventstore.Command) []error { +func pushAggregates(es *eventstore.Eventstore, aggregateCommands [][]eventstore.Command) []error { wg := sync.WaitGroup{} errs := make([]error, 0) errsMu := sync.Mutex{} @@ -619,7 +619,7 @@ func pushAggregates(pusher eventstore.Pusher, aggregateCommands [][]eventstore.C go func(events []eventstore.Command) { <-ctx.Done() - _, err := pusher.Push(context.Background(), events...) //nolint:contextcheck + _, err := es.Push(context.Background(), events...) //nolint:contextcheck if err != nil { errsMu.Lock() errs = append(errs, err) diff --git a/internal/eventstore/eventstore_test.go b/internal/eventstore/eventstore_test.go index 33e80892c5..ec28b9c551 100644 --- a/internal/eventstore/eventstore_test.go +++ b/internal/eventstore/eventstore_test.go @@ -330,6 +330,12 @@ func Test_eventData(t *testing.T) { } } +var _ Pusher = (*testPusher)(nil) + +func (repo *testPusher) Client() *database.DB { + return nil +} + type testPusher struct { events []Event errs []error @@ -341,7 +347,7 @@ func (repo *testPusher) Health(ctx context.Context) error { return nil } -func (repo *testPusher) Push(ctx context.Context, commands ...Command) (events []Event, err error) { +func (repo *testPusher) Push(_ context.Context, _ database.QueryExecuter, commands ...Command) (events []Event, err error) { if len(repo.errs) != 0 { err, repo.errs = repo.errs[0], repo.errs[1:] return nil, err diff --git a/internal/eventstore/repository/mock/repository.mock.go b/internal/eventstore/repository/mock/repository.mock.go index 58a6c8f86f..de04fef8c9 100644 --- a/internal/eventstore/repository/mock/repository.mock.go +++ b/internal/eventstore/repository/mock/repository.mock.go @@ -136,6 +136,20 @@ func (m *MockPusher) EXPECT() *MockPusherMockRecorder { return m.recorder } +// Client mocks base method. +func (m *MockPusher) Client() *database.DB { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Client") + ret0, _ := ret[0].(*database.DB) + return ret0 +} + +// Client indicates an expected call of Client. +func (mr *MockPusherMockRecorder) Client() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Client", reflect.TypeOf((*MockPusher)(nil).Client)) +} + // Health mocks base method. func (m *MockPusher) Health(arg0 context.Context) error { m.ctrl.T.Helper() @@ -151,10 +165,10 @@ func (mr *MockPusherMockRecorder) Health(arg0 any) *gomock.Call { } // Push mocks base method. -func (m *MockPusher) Push(arg0 context.Context, arg1 ...eventstore.Command) ([]eventstore.Event, error) { +func (m *MockPusher) Push(arg0 context.Context, arg1 database.QueryExecuter, arg2 ...eventstore.Command) ([]eventstore.Event, error) { m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { + varargs := []any{arg0, arg1} + for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Push", varargs...) @@ -164,8 +178,8 @@ func (m *MockPusher) Push(arg0 context.Context, arg1 ...eventstore.Command) ([]e } // Push indicates an expected call of Push. -func (mr *MockPusherMockRecorder) Push(arg0 any, arg1 ...any) *gomock.Call { +func (mr *MockPusherMockRecorder) Push(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockPusher)(nil).Push), varargs...) } diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index d41521ad8f..365da7afe2 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" ) @@ -78,8 +79,8 @@ func (m *MockRepository) ExpectInstanceIDsError(err error) *MockRepository { // ExpectPush checks if the expectedCommands are send to the Push method. // The call will sleep at least the amount of passed duration. func (m *MockRepository) ExpectPush(expectedCommands []eventstore.Command, sleep time.Duration) *MockRepository { - m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, commands ...eventstore.Command) ([]eventstore.Event, error) { + m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, _ database.QueryExecuter, commands ...eventstore.Command) ([]eventstore.Event, error) { m.MockPusher.ctrl.T.Helper() time.Sleep(sleep) @@ -133,8 +134,8 @@ func (m *MockRepository) ExpectPush(expectedCommands []eventstore.Command, sleep func (m *MockRepository) ExpectPushFailed(err error, expectedCommands []eventstore.Command) *MockRepository { m.MockPusher.ctrl.T.Helper() - m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, commands ...eventstore.Command) ([]eventstore.Event, error) { + m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, _ database.QueryExecuter, commands ...eventstore.Command) ([]eventstore.Event, error) { if len(expectedCommands) != len(commands) { return nil, fmt.Errorf("unexpected amount of commands: want %d, got %d", len(expectedCommands), len(commands)) } @@ -195,8 +196,8 @@ func (e *mockEvent) CreatedAt() time.Time { } func (m *MockRepository) ExpectRandomPush(expectedCommands []eventstore.Command) *MockRepository { - m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, commands ...eventstore.Command) ([]eventstore.Event, error) { + m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, _ database.QueryExecuter, commands ...eventstore.Command) ([]eventstore.Event, error) { assert.Len(m.MockPusher.ctrl.T, commands, len(expectedCommands)) events := make([]eventstore.Event, len(commands)) @@ -213,8 +214,8 @@ func (m *MockRepository) ExpectRandomPush(expectedCommands []eventstore.Command) } func (m *MockRepository) ExpectRandomPushFailed(err error, expectedEvents []eventstore.Command) *MockRepository { - m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, events ...eventstore.Command) ([]eventstore.Event, error) { + m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, _ database.QueryExecuter, events ...eventstore.Command) ([]eventstore.Event, error) { assert.Len(m.MockPusher.ctrl.T, events, len(expectedEvents)) return nil, err }, diff --git a/internal/eventstore/v3/eventstore.go b/internal/eventstore/v3/eventstore.go index 4ecaf6bad2..7c58f53f29 100644 --- a/internal/eventstore/v3/eventstore.go +++ b/internal/eventstore/v3/eventstore.go @@ -4,6 +4,7 @@ import ( "context" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" ) var ( @@ -11,12 +12,19 @@ var ( pushPlaceholderFmt string // uniqueConstraintPlaceholderFmt defines the format of the unique constraint error returned from the database uniqueConstraintPlaceholderFmt string + + _ eventstore.Pusher = (*Eventstore)(nil) ) type Eventstore struct { client *database.DB } +// Client implements the [eventstore.Pusher] +func (es *Eventstore) Client() *database.DB { + return es.client +} + func NewEventstore(client *database.DB) *Eventstore { switch client.Type() { case "cockroach": diff --git a/internal/eventstore/v3/field.go b/internal/eventstore/v3/field.go index 17037f8bcc..cfa9c08bba 100644 --- a/internal/eventstore/v3/field.go +++ b/internal/eventstore/v3/field.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -142,7 +143,7 @@ func buildSearchCondition(builder *strings.Builder, index int, conditions map[ev return args } -func handleFieldCommands(ctx context.Context, tx *sql.Tx, commands []eventstore.Command) error { +func handleFieldCommands(ctx context.Context, tx database.Tx, commands []eventstore.Command) error { for _, command := range commands { if len(command.Fields()) > 0 { if err := handleFieldOperations(ctx, tx, command.Fields()); err != nil { @@ -153,7 +154,7 @@ func handleFieldCommands(ctx context.Context, tx *sql.Tx, commands []eventstore. return nil } -func handleFieldFillEvents(ctx context.Context, tx *sql.Tx, events []eventstore.FillFieldsEvent) error { +func handleFieldFillEvents(ctx context.Context, tx database.Tx, events []eventstore.FillFieldsEvent) error { for _, event := range events { if len(event.Fields()) > 0 { if err := handleFieldOperations(ctx, tx, event.Fields()); err != nil { @@ -164,7 +165,7 @@ func handleFieldFillEvents(ctx context.Context, tx *sql.Tx, events []eventstore. return nil } -func handleFieldOperations(ctx context.Context, tx *sql.Tx, operations []*eventstore.FieldOperation) error { +func handleFieldOperations(ctx context.Context, tx database.Tx, operations []*eventstore.FieldOperation) error { for _, operation := range operations { if operation.Set != nil { if err := handleFieldSet(ctx, tx, operation.Set); err != nil { @@ -182,7 +183,7 @@ func handleFieldOperations(ctx context.Context, tx *sql.Tx, operations []*events return nil } -func handleFieldSet(ctx context.Context, tx *sql.Tx, field *eventstore.Field) error { +func handleFieldSet(ctx context.Context, tx database.Tx, field *eventstore.Field) error { if len(field.UpsertConflictFields) == 0 { return handleSearchInsert(ctx, tx, field) } @@ -193,7 +194,7 @@ const ( insertField = `INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)` ) -func handleSearchInsert(ctx context.Context, tx *sql.Tx, field *eventstore.Field) error { +func handleSearchInsert(ctx context.Context, tx database.Tx, field *eventstore.Field) error { value, err := json.Marshal(field.Value.Value) if err != nil { return zerrors.ThrowInvalidArgument(err, "V3-fcrW1", "unable to marshal field value") @@ -222,7 +223,7 @@ const ( fieldsUpsertSuffix = ` RETURNING * ) INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 WHERE NOT EXISTS (SELECT 1 FROM upsert)` ) -func handleSearchUpsert(ctx context.Context, tx *sql.Tx, field *eventstore.Field) error { +func handleSearchUpsert(ctx context.Context, tx database.Tx, field *eventstore.Field) error { value, err := json.Marshal(field.Value.Value) if err != nil { return zerrors.ThrowInvalidArgument(err, "V3-fcrW1", "unable to marshal field value") @@ -268,7 +269,7 @@ func writeUpsertField(fields []eventstore.FieldType) string { const removeSearch = `DELETE FROM eventstore.fields WHERE ` -func handleSearchDelete(ctx context.Context, tx *sql.Tx, clauses map[eventstore.FieldType]any) error { +func handleSearchDelete(ctx context.Context, tx database.Tx, clauses map[eventstore.FieldType]any) error { if len(clauses) == 0 { return zerrors.ThrowInvalidArgument(nil, "V3-oqlBZ", "no conditions") } diff --git a/internal/eventstore/v3/push.go b/internal/eventstore/v3/push.go index 47a4c96dca..c0f66209c3 100644 --- a/internal/eventstore/v3/push.go +++ b/internal/eventstore/v3/push.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -22,13 +23,45 @@ import ( var appNamePrefix = dialect.DBPurposeEventPusher.AppName() + "_" -func (es *Eventstore) Push(ctx context.Context, commands ...eventstore.Command) (events []eventstore.Event, err error) { +var pushTxOpts = &sql.TxOptions{ + Isolation: sql.LevelReadCommitted, + ReadOnly: false, +} + +func (es *Eventstore) Push(ctx context.Context, client database.QueryExecuter, commands ...eventstore.Command) (events []eventstore.Event, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - tx, err := es.client.BeginTx(ctx, nil) - if err != nil { - return nil, err + var tx database.Tx + switch c := client.(type) { + case database.Tx: + tx = c + case database.Client: + // We cannot use READ COMMITTED on CockroachDB because we use cluster_logical_timestamp() which is not supported in this isolation level + var opts *sql.TxOptions + if es.client.Database.Type() == "postgres" { + opts = pushTxOpts + } + tx, err = c.BeginTx(ctx, opts) + if err != nil { + return nil, err + } + defer func() { + err = database.CloseTransaction(tx, err) + }() + default: + // We cannot use READ COMMITTED on CockroachDB because we use cluster_logical_timestamp() which is not supported in this isolation level + var opts *sql.TxOptions + if es.client.Database.Type() == "postgres" { + opts = pushTxOpts + } + tx, err = es.client.BeginTx(ctx, opts) + if err != nil { + return nil, err + } + defer func() { + err = database.CloseTransaction(tx, err) + }() } // tx is not closed because [crdb.ExecuteInTx] takes care of that var ( @@ -42,43 +75,30 @@ func (es *Eventstore) Push(ctx context.Context, commands ...eventstore.Command) return nil, err } - // needs to be set like this because psql complains about parameters in the SET statement - _, err = tx.ExecContext(ctx, "SET application_name = '"+appNamePrefix+authz.GetInstance(ctx).InstanceID()+"'") + sequences, err = latestSequences(ctx, tx, commands) if err != nil { - logging.WithError(err).Warn("failed to set application name") return nil, err } - err = crdb.ExecuteInTx(ctx, &transaction{tx}, func() (err error) { - inTxCtx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() + events, err = insertEvents(ctx, tx, sequences, commands) + if err != nil { + return nil, err + } - sequences, err = latestSequences(inTxCtx, tx, commands) + if err = handleUniqueConstraints(ctx, tx, commands); err != nil { + return nil, err + } + + // CockroachDB by default does not allow multiple modifications of the same table using ON CONFLICT + // Thats why we enable it manually + if es.client.Type() == "cockroach" { + _, err = tx.Exec("SET enable_multiple_modifications_of_table = on") if err != nil { - return err + return nil, err } + } - events, err = insertEvents(inTxCtx, tx, sequences, commands) - if err != nil { - return err - } - - if err = handleUniqueConstraints(inTxCtx, tx, commands); err != nil { - return err - } - - // CockroachDB by default does not allow multiple modifications of the same table using ON CONFLICT - // Thats why we enable it manually - if es.client.Type() == "cockroach" { - _, err = tx.Exec("SET enable_multiple_modifications_of_table = on") - if err != nil { - return err - } - } - - return handleFieldCommands(inTxCtx, tx, commands) - }) - + err = handleFieldCommands(ctx, tx, commands) if err != nil { return nil, err } @@ -89,7 +109,7 @@ func (es *Eventstore) Push(ctx context.Context, commands ...eventstore.Command) //go:embed push.sql var pushStmt string -func insertEvents(ctx context.Context, tx *sql.Tx, sequences []*latestSequence, commands []eventstore.Command) ([]eventstore.Event, error) { +func insertEvents(ctx context.Context, tx database.Tx, sequences []*latestSequence, commands []eventstore.Command) ([]eventstore.Event, error) { events, placeholders, args, err := mapCommands(commands, sequences) if err != nil { return nil, err @@ -186,7 +206,7 @@ func mapCommands(commands []eventstore.Command, sequences []*latestSequence) (ev } type transaction struct { - *sql.Tx + database.Tx } var _ crdb.Tx = (*transaction)(nil) diff --git a/internal/eventstore/v3/sequence.go b/internal/eventstore/v3/sequence.go index 8d84ef4755..7d97e1080d 100644 --- a/internal/eventstore/v3/sequence.go +++ b/internal/eventstore/v3/sequence.go @@ -10,6 +10,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -22,7 +23,7 @@ type latestSequence struct { //go:embed sequences_query.sql var latestSequencesStmt string -func latestSequences(ctx context.Context, tx *sql.Tx, commands []eventstore.Command) ([]*latestSequence, error) { +func latestSequences(ctx context.Context, tx database.Tx, commands []eventstore.Command) ([]*latestSequence, error) { sequences := commandsToSequences(ctx, commands) conditions, args := sequencesToSql(sequences) diff --git a/internal/eventstore/v3/unique_constraints.go b/internal/eventstore/v3/unique_constraints.go index e3bae89805..9c4d1831c4 100644 --- a/internal/eventstore/v3/unique_constraints.go +++ b/internal/eventstore/v3/unique_constraints.go @@ -2,7 +2,6 @@ package eventstore import ( "context" - "database/sql" _ "embed" "errors" "fmt" @@ -11,6 +10,7 @@ import ( "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -24,7 +24,7 @@ var ( addConstraintStmt string ) -func handleUniqueConstraints(ctx context.Context, tx *sql.Tx, commands []eventstore.Command) error { +func handleUniqueConstraints(ctx context.Context, tx database.Tx, commands []eventstore.Command) error { deletePlaceholders := make([]string, 0) deleteArgs := make([]any, 0) From 7714af6f5b871ccb2a61e1da4ea64dd7dadb8763 Mon Sep 17 00:00:00 2001 From: Silvan Date: Mon, 25 Nov 2024 07:02:59 +0100 Subject: [PATCH 23/32] fix(eventstore): correct database type in `PushWithClient` (#8949) # Which Problems Are Solved `eventstore.PushWithClient` required the wrong type of for the client parameter. # How the Problems Are Solved Changed type of client from `database.Client` to `database.QueryExecutor` --- internal/eventstore/eventstore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index 6246978739..22dfde3f4f 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -90,7 +90,7 @@ func (es *Eventstore) Push(ctx context.Context, cmds ...Command) ([]Event, error // PushWithClient pushes the events in a single transaction using the provided database client // an event needs at least an aggregate -func (es *Eventstore) PushWithClient(ctx context.Context, client database.Client, cmds ...Command) ([]Event, error) { +func (es *Eventstore) PushWithClient(ctx context.Context, client database.QueryExecuter, cmds ...Command) ([]Event, error) { if es.PushTimeout > 0 { var cancel func() ctx, cancel = context.WithTimeout(ctx, es.PushTimeout) From ae49b390a22eb777741be49b599ccc23a8dc8f69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:04:30 +0100 Subject: [PATCH 24/32] chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /console (#8941) Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
Changelog

Sourced from cross-spawn's changelog.

7.0.6 (2024-11-18)

Bug Fixes

  • update cross-spawn version to 7.0.5 in package-lock.json (f700743)

7.0.5 (2024-11-07)

Bug Fixes

  • fix escaping bug introduced by backtracking (640d391)

7.0.4 (2024-11-07)

Bug Fixes

Commits
  • 77cd97f chore(release): 7.0.6
  • 6717de4 chore: upgrade standard-version
  • f700743 fix: update cross-spawn version to 7.0.5 in package-lock.json
  • 9a7e3b2 chore: fix build status badge
  • 0852683 chore(release): 7.0.5
  • 640d391 fix: fix escaping bug introduced by backtracking
  • bff0c87 chore: remove codecov
  • a7c6abc chore: replace travis with github workflows
  • 9b9246e chore(release): 7.0.4
  • 5ff3a07 fix: disable regexp backtracking (#160)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cross-spawn&package-manager=npm_and_yarn&previous-version=7.0.3&new-version=7.0.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/zitadel/zitadel/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Max Peintner --- console/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/console/yarn.lock b/console/yarn.lock index 74e8599a93..c54f09c395 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -4537,9 +4537,9 @@ critters@0.0.20: pretty-bytes "^5.3.0" cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From 91290d6195cb7e91c5780cc51c3215d218e50a5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 08:30:09 +0000 Subject: [PATCH 25/32] chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /docs (#8925) Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
Changelog

Sourced from cross-spawn's changelog.

7.0.6 (2024-11-18)

Bug Fixes

  • update cross-spawn version to 7.0.5 in package-lock.json (f700743)

7.0.5 (2024-11-07)

Bug Fixes

  • fix escaping bug introduced by backtracking (640d391)

7.0.4 (2024-11-07)

Bug Fixes

Commits
  • 77cd97f chore(release): 7.0.6
  • 6717de4 chore: upgrade standard-version
  • f700743 fix: update cross-spawn version to 7.0.5 in package-lock.json
  • 9a7e3b2 chore: fix build status badge
  • 0852683 chore(release): 7.0.5
  • 640d391 fix: fix escaping bug introduced by backtracking
  • bff0c87 chore: remove codecov
  • a7c6abc chore: replace travis with github workflows
  • 9b9246e chore(release): 7.0.4
  • 5ff3a07 fix: disable regexp backtracking (#160)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cross-spawn&package-manager=npm_and_yarn&previous-version=7.0.3&new-version=7.0.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/zitadel/zitadel/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Max Peintner --- docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index b5d92b98b4..94698e4821 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3909,9 +3909,9 @@ cross-fetch@3.1.5: node-fetch "2.6.7" cross-spawn@^7.0.0, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From ff70ede7c78fc3ffa36aa2639a896597e89a786b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 25 Nov 2024 17:25:11 +0200 Subject: [PATCH 26/32] feat(eventstore): exclude aggregate IDs when event_type occurred (#8940) # Which Problems Are Solved For truly event-based notification handler, we need to be able to filter out events of aggregates which are already handled. For example when an event like `notify.success` or `notify.failed` was created on an aggregate, we no longer require events from that aggregate ID. # How the Problems Are Solved Extend the query builder to use a `NOT IN` clause which excludes aggregate IDs when they have certain events for a certain aggregate type. For optimization and proper index usages, certain filters are inherited from the parent query, such as: - Instance ID - Instance IDs - Position offset This is a prettified query as used by the unit tests: ```sql SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN ( SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8 ) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9 ``` I used this query to run it against the `oidc_session` aggregate looking for added events, excluding aggregates where a token was revoked, against a recent position. It fully used index scans:
```json [ { "Plan": { "Node Type": "Index Scan", "Parallel Aware": false, "Async Capable": false, "Scan Direction": "Forward", "Index Name": "es_projection", "Relation Name": "events2", "Alias": "events2", "Actual Rows": 2, "Actual Loops": 1, "Index Cond": "((instance_id = '286399006995644420'::text) AND (aggregate_type = 'oidc_session'::text) AND (event_type = 'oidc_session.added'::text) AND (\"position\" > 1731582100.784168))", "Rows Removed by Index Recheck": 0, "Filter": "(NOT (hashed SubPlan 1))", "Rows Removed by Filter": 1, "Plans": [ { "Node Type": "Index Scan", "Parent Relationship": "SubPlan", "Subplan Name": "SubPlan 1", "Parallel Aware": false, "Async Capable": false, "Scan Direction": "Forward", "Index Name": "es_projection", "Relation Name": "events2", "Alias": "events2_1", "Actual Rows": 1, "Actual Loops": 1, "Index Cond": "((instance_id = '286399006995644420'::text) AND (aggregate_type = 'oidc_session'::text) AND (event_type = 'oidc_session.access_token.revoked'::text) AND (\"position\" > 1731582100.784168))", "Rows Removed by Index Recheck": 0 } ] }, "Triggers": [ ] } ] ```
# Additional Changes - None # Additional Context - Related to https://github.com/zitadel/zitadel/issues/8931 --------- Co-authored-by: adlerhurst --- .../eventstore/eventstore_querier_test.go | 33 ++++++++ .../eventstore/repository/search_query.go | 54 ++++++++++--- internal/eventstore/repository/sql/query.go | 17 +++++ .../eventstore/repository/sql/query_test.go | 76 ++++++++++++++++++- internal/eventstore/search_query.go | 46 +++++++++++ 5 files changed, 216 insertions(+), 10 deletions(-) diff --git a/internal/eventstore/eventstore_querier_test.go b/internal/eventstore/eventstore_querier_test.go index 856bb4a20e..4b7ad78b25 100644 --- a/internal/eventstore/eventstore_querier_test.go +++ b/internal/eventstore/eventstore_querier_test.go @@ -66,6 +66,39 @@ func TestCRDB_Filter(t *testing.T) { }, wantErr: false, }, + { + name: "exclude aggregate type and event type", + args: args{ + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(eventstore.AggregateType(t.Name())). + Builder(). + ExcludeAggregateIDs(). + EventTypes("test.updated"). + AggregateTypes(eventstore.AggregateType(t.Name())). + Builder(), + }, + fields: fields{ + existingEvents: []eventstore.Command{ + generateCommand(eventstore.AggregateType(t.Name()), "306"), + generateCommand( + eventstore.AggregateType(t.Name()), + "306", + func(te *testEvent) { + te.EventType = "test.updated" + }, + ), + generateCommand( + eventstore.AggregateType(t.Name()), + "308", + ), + }, + }, + res: res{ + eventCount: 1, + }, + wantErr: false, + }, } for _, tt := range tests { for querierName, querier := range queriers { diff --git a/internal/eventstore/repository/search_query.go b/internal/eventstore/repository/search_query.go index 32a353049d..f84c7f1201 100644 --- a/internal/eventstore/repository/search_query.go +++ b/internal/eventstore/repository/search_query.go @@ -22,15 +22,16 @@ type SearchQuery struct { Offset uint32 Desc bool - InstanceID *Filter - InstanceIDs *Filter - ExcludedInstances *Filter - Creator *Filter - Owner *Filter - Position *Filter - Sequence *Filter - CreatedAfter *Filter - CreatedBefore *Filter + InstanceID *Filter + InstanceIDs *Filter + ExcludedInstances *Filter + Creator *Filter + Owner *Filter + Position *Filter + Sequence *Filter + CreatedAfter *Filter + CreatedBefore *Filter + ExcludeAggregateIDs []*Filter } // Filter represents all fields needed to compare a field of an event with a value @@ -171,6 +172,21 @@ func QueryFromBuilder(builder *eventstore.SearchQueryBuilder) (*SearchQuery, err query.SubQueries[i] = append(query.SubQueries[i], filter) } } + if excludeAggregateIDs := builder.GetExcludeAggregateIDs(); excludeAggregateIDs != nil { + for _, f := range []func(query *eventstore.ExclusionQuery) *Filter{ + excludeAggregateTypeFilter, + excludeEventTypeFilter, + } { + filter := f(excludeAggregateIDs) + if filter == nil { + continue + } + if err := filter.Validate(); err != nil { + return nil, err + } + query.ExcludeAggregateIDs = append(query.ExcludeAggregateIDs, filter) + } + } return query, nil } @@ -286,3 +302,23 @@ func eventPositionAfterFilter(query *eventstore.SearchQuery) *Filter { } return nil } + +func excludeEventTypeFilter(query *eventstore.ExclusionQuery) *Filter { + if len(query.GetEventTypes()) < 1 { + return nil + } + if len(query.GetEventTypes()) == 1 { + return NewFilter(FieldEventType, query.GetEventTypes()[0], OperationEquals) + } + return NewFilter(FieldEventType, database.TextArray[eventstore.EventType](query.GetEventTypes()), OperationIn) +} + +func excludeAggregateTypeFilter(query *eventstore.ExclusionQuery) *Filter { + if len(query.GetAggregateTypes()) < 1 { + return nil + } + if len(query.GetAggregateTypes()) == 1 { + return NewFilter(FieldAggregateType, query.GetAggregateTypes()[0], OperationEquals) + } + return NewFilter(FieldAggregateType, database.TextArray[eventstore.AggregateType](query.GetAggregateTypes()), OperationIn) +} diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index dda12187f9..01f0c080e3 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -283,6 +283,23 @@ func prepareConditions(criteria querier, query *repository.SearchQuery, useV1 bo args = append(args, additionalArgs...) } + excludeAggregateIDs := query.ExcludeAggregateIDs + if len(excludeAggregateIDs) > 0 { + excludeAggregateIDs = append(excludeAggregateIDs, query.InstanceID, query.InstanceIDs, query.Position) + } + excludeAggregateIDsClauses, excludeAggregateIDsArgs := prepareQuery(criteria, useV1, excludeAggregateIDs...) + if excludeAggregateIDsClauses != "" { + if clauses != "" { + clauses += " AND " + } + if useV1 { + clauses += "aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE " + excludeAggregateIDsClauses + ")" + } else { + clauses += "aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE " + excludeAggregateIDsClauses + ")" + } + args = append(args, excludeAggregateIDsArgs...) + } + if query.AwaitOpenTransactions { instanceIDs := make(database.TextArray[string], 0, 3) if query.InstanceID != nil { diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index a3d47b838d..2caa9da72d 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "database/sql/driver" "reflect" + "regexp" "strconv" "testing" "time" @@ -744,6 +745,7 @@ func Test_query_events_mocked(t *testing.T) { type args struct { query *eventstore.SearchQueryBuilder dest interface{} + useV1 bool } type res struct { wantErr bool @@ -767,6 +769,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder(), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQuery(t, @@ -789,6 +792,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder(), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQuery(t, @@ -811,6 +815,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder(), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQuery(t, @@ -834,6 +839,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder(), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQuery(t, @@ -855,6 +861,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder().LockRowsDuringTx(nil, eventstore.LockOptionWait), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQuery(t, @@ -876,6 +883,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder().LockRowsDuringTx(nil, eventstore.LockOptionNoWait), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQuery(t, @@ -897,6 +905,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder().LockRowsDuringTx(nil, eventstore.LockOptionSkipLocked), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQuery(t, @@ -919,6 +928,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder(), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQueryErr(t, @@ -941,6 +951,7 @@ func Test_query_events_mocked(t *testing.T) { AddQuery(). AggregateTypes("user"). Builder(), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQueryScanErr(t, @@ -975,6 +986,7 @@ func Test_query_events_mocked(t *testing.T) { AggregateTypes("org"). AggregateIDs("asdf42"). Builder(), + useV1: true, }, fields: fields{ mock: newMockClient(t).expectQuery(t, @@ -986,6 +998,68 @@ func Test_query_events_mocked(t *testing.T) { wantErr: false, }, }, + { + name: "aggregate / event type, position and exclusion, v1", + args: args{ + dest: &[]*repository.Event{}, + query: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + InstanceID("instanceID"). + OrderDesc(). + Limit(5). + PositionAfter(123.456). + AddQuery(). + AggregateTypes("notify"). + EventTypes("notify.foo.bar"). + Builder(). + ExcludeAggregateIDs(). + AggregateTypes("notify"). + EventTypes("notification.failed", "notification.success"). + Builder(), + useV1: true, + }, + fields: fields{ + mock: newMockClient(t).expectQuery(t, + regexp.QuoteMeta( + `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY event_sequence DESC LIMIT $9`, + ), + []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, + ), + }, + res: res{ + wantErr: false, + }, + }, + { + name: "aggregate / event type, position and exclusion, v2", + args: args{ + dest: &[]*repository.Event{}, + query: eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + InstanceID("instanceID"). + OrderDesc(). + Limit(5). + PositionAfter(123.456). + AddQuery(). + AggregateTypes("notify"). + EventTypes("notify.foo.bar"). + Builder(). + ExcludeAggregateIDs(). + AggregateTypes("notify"). + EventTypes("notification.failed", "notification.success"). + Builder(), + useV1: false, + }, + fields: fields{ + mock: newMockClient(t).expectQuery(t, + regexp.QuoteMeta( + `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`, + ), + []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, + ), + }, + res: res{ + wantErr: false, + }, + }, } crdb := NewCRDB(&database.DB{Database: new(testDB)}) for _, tt := range tests { @@ -994,7 +1068,7 @@ func Test_query_events_mocked(t *testing.T) { crdb.DB.DB = tt.fields.mock.client } - err := query(context.Background(), crdb, tt.args.query, tt.args.dest, true) + err := query(context.Background(), crdb, tt.args.query, tt.args.dest, tt.args.useV1) if (err != nil) != tt.res.wantErr { t.Errorf("query() error = %v, wantErr %v", err, tt.res.wantErr) } diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index 58e25f6567..df38d15def 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -21,6 +21,7 @@ type SearchQueryBuilder struct { instanceIDs []string editorUser string queries []*SearchQuery + excludeAggregateIDs *ExclusionQuery tx *sql.Tx lockRows bool lockOption LockOption @@ -68,6 +69,10 @@ func (b *SearchQueryBuilder) GetQueries() []*SearchQuery { return b.queries } +func (b *SearchQueryBuilder) GetExcludeAggregateIDs() *ExclusionQuery { + return b.excludeAggregateIDs +} + func (b *SearchQueryBuilder) GetTx() *sql.Tx { return b.tx } @@ -136,6 +141,20 @@ func (q SearchQuery) GetPositionAfter() float64 { return q.positionAfter } +type ExclusionQuery struct { + builder *SearchQueryBuilder + aggregateTypes []AggregateType + eventTypes []EventType +} + +func (q ExclusionQuery) GetAggregateTypes() []AggregateType { + return q.aggregateTypes +} + +func (q ExclusionQuery) GetEventTypes() []EventType { + return q.eventTypes +} + // Columns defines which fields of the event are needed for the query type Columns int8 @@ -346,6 +365,16 @@ func (builder *SearchQueryBuilder) AddQuery() *SearchQuery { return query } +// ExcludeAggregateIDs excludes events from the aggregate IDs returned by the [ExclusionQuery]. +// There can be only 1 exclusion query. Subsequent calls overwrite previous definitions. +func (builder *SearchQueryBuilder) ExcludeAggregateIDs() *ExclusionQuery { + query := &ExclusionQuery{ + builder: builder, + } + builder.excludeAggregateIDs = query + return query +} + // Or creates a new sub query on the search query builder func (query SearchQuery) Or() *SearchQuery { return query.builder.AddQuery() @@ -398,3 +427,20 @@ func (query *SearchQuery) matches(command Command) bool { } return true } + +// AggregateTypes filters for events with the given aggregate types +func (query *ExclusionQuery) AggregateTypes(types ...AggregateType) *ExclusionQuery { + query.aggregateTypes = types + return query +} + +// EventTypes filters for events with the given event types +func (query *ExclusionQuery) EventTypes(types ...EventType) *ExclusionQuery { + query.eventTypes = types + return query +} + +// Builder returns the SearchQueryBuilder of the sub query +func (query *ExclusionQuery) Builder() *SearchQueryBuilder { + return query.builder +} From 33bff5a4b0883b0b8e97be1818f5c8bd348100e6 Mon Sep 17 00:00:00 2001 From: Luka Waymouth <155040393+lukawaay@users.noreply.github.com> Date: Tue, 26 Nov 2024 04:00:21 -0500 Subject: [PATCH 27/32] fix(console): bug fixes for ListProjectRoles and general pagination (#8938) # Which Problems Are Solved A number of small problems are fixed relating to the project roles listed in various places in the UI: - Fixes issue #8460 - Fixes an issue where the "Master checkbox" that's supposed to check and uncheck all list items breaks when there's multiple pages of results. Demonstration images are attached at the end of the PR. - Fixes an issue where the "Edit Role" dialog opened by clicking on a role in the list will not save any changes if the role's group is empty even though empty groups are allowed during creation. - Fixes issues where the list does not properly update after the user modifies or deletes some of its entries. - Fixes an issue for all paginated lists where the page number information (like "0-25" specifying that items 0 through 25 are shown on screen) was inaccurate, as described in #8460. # How the Problems Are Solved - Fixes buggy handling of pre-selected roles while editing a grant so that all selected roles are saved instead of only the ones on the current page. - Triggers the entire page to be reloaded when a user modifies or deletes a role to easily ensure the information on the screen is accurate. - Revises checkbox logic so that the "Master checkbox" will apply only to rows on the current page. I think this is the correct behavior but tell me if it should be changed. - Other fixes to faulty logic. # Additional Changes - I made clicking on a group name toggle all the rows in that group on the screen, instead of just turning them on. Tell me if this should be changed back to what it was before. # Additional Context - Closes #8460 ## An example of the broken checkboxes: ![2024-11-20_03-11-1732091377](https://github.com/user-attachments/assets/9f01f529-aac9-4669-92df-2abbe67e4983) ![2024-11-20_03-11-1732091365](https://github.com/user-attachments/assets/e7b8bed6-5cef-4c9f-9ecf-45ed41640dc6) ![2024-11-20_03-11-1732091357](https://github.com/user-attachments/assets/d404bc78-68fd-472d-b450-6578658f48ab) ![2024-11-20_03-11-1732091348](https://github.com/user-attachments/assets/a5976816-802b-4eab-bc61-58babc0b68f7) --------- Co-authored-by: Max Peintner --- .../paginator/paginator.component.html | 6 +- .../modules/paginator/paginator.component.ts | 15 ++++ .../project-role-detail-dialog.component.ts | 4 +- .../project-roles-table.component.html | 8 +- .../project-roles-table.component.ts | 83 ++++++++++--------- 5 files changed, 65 insertions(+), 51 deletions(-) diff --git a/console/src/app/modules/paginator/paginator.component.html b/console/src/app/modules/paginator/paginator.component.html index 9138038437..e74a3e3881 100644 --- a/console/src/app/modules/paginator/paginator.component.html +++ b/console/src/app/modules/paginator/paginator.component.html @@ -8,12 +8,10 @@

- {{ pageIndex * pageSize }} - {{ pageIndex * pageSize + pageSize }} - + {{ startIndex }} - {{ endIndex }}
- + {{ sizeOption }} diff --git a/console/src/app/modules/paginator/paginator.component.ts b/console/src/app/modules/paginator/paginator.component.ts index b4b406acda..4fd62fb568 100644 --- a/console/src/app/modules/paginator/paginator.component.ts +++ b/console/src/app/modules/paginator/paginator.component.ts @@ -50,6 +50,15 @@ export class PaginatorComponent { return temp <= this.length / this.pageSize; } + get startIndex(): number { + return this.pageIndex * this.pageSize; + } + + get endIndex(): number { + const max = this.startIndex + this.pageSize; + return this.length < max ? this.length : max; + } + public emitChange(): void { this.page.emit({ length: this.length, @@ -58,4 +67,10 @@ export class PaginatorComponent { pageSizeOptions: this.pageSizeOptions, }); } + + public updatePageSize(newSize: number): void { + this.pageSize = newSize; + this.pageIndex = 0; + this.emitChange(); + } } diff --git a/console/src/app/modules/project-role-detail-dialog/project-role-detail-dialog.component.ts b/console/src/app/modules/project-role-detail-dialog/project-role-detail-dialog.component.ts index 24c7002778..f62ccc54a1 100644 --- a/console/src/app/modules/project-role-detail-dialog/project-role-detail-dialog.component.ts +++ b/console/src/app/modules/project-role-detail-dialog/project-role-detail-dialog.component.ts @@ -31,9 +31,9 @@ export class ProjectRoleDetailDialogComponent { } submitForm(): void { - if (this.formGroup.valid && this.key?.value && this.group?.value && this.displayName?.value) { + if (this.formGroup.valid && this.key?.value && this.displayName?.value) { this.mgmtService - .updateProjectRole(this.projectId, this.key.value, this.displayName.value, this.group.value) + .updateProjectRole(this.projectId, this.key.value, this.displayName.value, this.group?.value) .then(() => { this.toast.showInfo('PROJECT.TOAST.ROLECHANGED', true); this.dialogRef.close(true); diff --git a/console/src/app/modules/project-roles-table/project-roles-table.component.html b/console/src/app/modules/project-roles-table/project-roles-table.component.html index 851e6848aa..48ad2851bb 100644 --- a/console/src/app/modules/project-roles-table/project-roles-table.component.html +++ b/console/src/app/modules/project-roles-table/project-roles-table.component.html @@ -35,8 +35,8 @@ [disabled]="disabled" color="primary" (change)="$event ? masterToggle() : null" - [checked]="selection.hasValue() && isAllSelected()" - [indeterminate]="selection.hasValue() && !isAllSelected()" + [checked]="isAnySelected() && isAllSelected()" + [indeterminate]="isAnySelected() && !isAllSelected()" >
@@ -76,7 +76,7 @@ class="role state" [ngClass]="{ 'no-selection': !selectionAllowed }" *ngIf="role.group" - (click)="selectionAllowed ? selectAllOfGroup(role.group) : openDetailDialog(role)" + (click)="selectionAllowed ? groupMasterToggle(role.group) : openDetailDialog(role)" [matTooltip]="selectionAllowed ? ('PROJECT.ROLE.SELECTGROUPTOOLTIP' | translate: role) : null" >{{ role.group }} @@ -135,7 +135,7 @@ #paginator [timestamp]="dataSource.viewTimestamp" [length]="dataSource.totalResult" - [pageSize]="50" + [pageSize]="INITIAL_PAGE_SIZE" (page)="changePage()" [pageSizeOptions]="[25, 50, 100, 250]" > diff --git a/console/src/app/modules/project-roles-table/project-roles-table.component.ts b/console/src/app/modules/project-roles-table/project-roles-table.component.ts index ae8633d15d..2159782411 100644 --- a/console/src/app/modules/project-roles-table/project-roles-table.component.ts +++ b/console/src/app/modules/project-roles-table/project-roles-table.component.ts @@ -18,6 +18,7 @@ import { ProjectRolesDataSource } from './project-roles-table-datasource'; styleUrls: ['./project-roles-table.component.scss'], }) export class ProjectRolesTableComponent implements OnInit { + public INITIAL_PAGE_SIZE: number = 50; @Input() public projectId: string = ''; @Input() public grantId: string = ''; @Input() public disabled: boolean = false; @@ -43,41 +44,58 @@ export class ProjectRolesTableComponent implements OnInit { } public ngOnInit(): void { - this.dataSource.loadRoles(this.projectId, this.grantId, 0, 25, 'asc'); - - this.dataSource.rolesSubject.subscribe((roles) => { - const selectedRoles: Role.AsObject[] = roles.filter((role) => this.selectedKeys.includes(role.key)); - this.selection.select(...selectedRoles.map((r) => r.key)); - }); + this.loadRolesPage(); + this.selection.select(...this.selectedKeys); this.selection.changed.subscribe(() => { this.changedSelection.emit(this.selection.selected); }); } - public selectAllOfGroup(group: string): void { - const groupRoles: Role.AsObject[] = this.dataSource.rolesSubject.getValue().filter((role) => role.group === group); - this.selection.select(...groupRoles.map((r) => r.key)); - } - private loadRolesPage(): void { - this.dataSource.loadRoles(this.projectId, this.grantId, this.paginator?.pageIndex ?? 0, this.paginator?.pageSize ?? 25); + this.dataSource.loadRoles( + this.projectId, + this.grantId, + this.paginator?.pageIndex ?? 0, + this.paginator?.pageSize ?? this.INITIAL_PAGE_SIZE, + ); } public changePage(): void { this.loadRolesPage(); } - public isAllSelected(): boolean { - const numSelected = this.selection.selected.length; - const numRows = this.dataSource.totalResult; - return numSelected === numRows; + private listIsAllSelected(list: string[]): boolean { + return list.findIndex((key) => !this.selection.isSelected(key)) == -1; + } + + private listIsAnySelected(list: string[]): boolean { + return list.findIndex((key) => this.selection.isSelected(key)) != -1; + } + + private listMasterToggle(list: string[]): void { + if (this.listIsAllSelected(list)) this.selection.deselect(...list); + else this.selection.select(...list); + } + + private compilePageKeys(): string[] { + return this.dataSource.rolesSubject.value.map((role) => role.key); } public masterToggle(): void { - this.isAllSelected() - ? this.selection.clear() - : this.dataSource.rolesSubject.value.forEach((row: Role.AsObject) => this.selection.select(row.key)); + this.listMasterToggle(this.compilePageKeys()); + } + + public isAllSelected(): boolean { + return this.listIsAllSelected(this.compilePageKeys()); + } + + public isAnySelected(): boolean { + return this.listIsAnySelected(this.compilePageKeys()); + } + + public groupMasterToggle(group: string): void { + this.listMasterToggle(this.dataSource.rolesSubject.value.filter((role) => role.group == group).map((role) => role.key)); } public deleteRole(role: Role.AsObject): void { @@ -93,45 +111,28 @@ export class ProjectRolesTableComponent implements OnInit { dialogRef.afterClosed().subscribe((resp) => { if (resp) { - const index = this.dataSource.rolesSubject.value.findIndex((iter) => iter.key === role.key); - this.mgmtService.removeProjectRole(this.projectId, role.key).then(() => { this.toast.showInfo('PROJECT.TOAST.ROLEREMOVED', true); - - if (index > -1) { - this.dataSource.rolesSubject.value.splice(index, 1); - this.dataSource.rolesSubject.next(this.dataSource.rolesSubject.value); - } + this.loadRolesPage(); }); } }); } - public removeRole(role: Role.AsObject, index: number): void { - this.mgmtService - .removeProjectRole(this.projectId, role.key) - .then(() => { - this.toast.showInfo('PROJECT.TOAST.ROLEREMOVED', true); - this.dataSource.rolesSubject.value.splice(index, 1); - this.dataSource.rolesSubject.next(this.dataSource.rolesSubject.value); - }) - .catch((error) => { - this.toast.showError(error); - }); - } - public openDetailDialog(role: Role.AsObject): void { - this.dialog.open(ProjectRoleDetailDialogComponent, { + const dialogRef = this.dialog.open(ProjectRoleDetailDialogComponent, { data: { role, projectId: this.projectId, }, width: '400px', }); + + dialogRef.afterClosed().subscribe(() => this.loadRolesPage()); } public refreshPage(): void { - this.dataSource.loadRoles(this.projectId, this.grantId, this.paginator?.pageIndex ?? 0, this.paginator?.pageSize ?? 25); + this.loadRolesPage(); } public get selectionAllowed(): boolean { From 1e0996371edf48125ac8d6b64427abbcf2202522 Mon Sep 17 00:00:00 2001 From: Martin Mayer <38588055+damattl@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:40:35 +0100 Subject: [PATCH 28/32] docs: update flutter dependencies (#8862) The dependencies in the [docs](https://zitadel.com/docs/examples/login/flutter) did not match the ones used in the code [example](https://github.com/zitadel/zitadel_flutter/blob/main/lib/main.dart#L14-L28) Co-authored-by: Max Peintner --- docs/docs/examples/login/flutter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/examples/login/flutter.md b/docs/docs/examples/login/flutter.md index 87822a01d6..f6e8352831 100644 --- a/docs/docs/examples/login/flutter.md +++ b/docs/docs/examples/login/flutter.md @@ -75,8 +75,8 @@ To install run: ```bash flutter pub add http -flutter pub add flutter_web_auth_2 -flutter pub add flutter_secure_storage +flutter pub add oidc +flutter pub add oidc_default_store ``` #### Setup for Android From cdd4f37ffadfb4f06b7a07e24e5b5a52969c09b4 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Tue, 26 Nov 2024 12:55:17 +0100 Subject: [PATCH 29/32] chore: improve adopters file (#8966) # Which Problems Are Solved This improves the `ADOPTERS.md` file to better understand its purpose. # How the Problems Are Solved Adding additional instructions to the `ADOPTERS.md` file --- ADOPTERS.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index 3bb7e1a707..377a4f6461 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,9 +1,15 @@ ## Adopters -We are grateful to the organizations and individuals who are using ZITADEL. If you are using ZITADEL, please consider adding your name to this list by submitting a pull request. +Sharing experiences and learning from other users is essential. We are frequently asked who is using a particular feature of Zitadel so people can get in touch with other users to share experiences and best practices. People also often want to know if a specific product or platform has integrated Zitadel. While the Zitadel Discord Community allows users to get in touch, it can be challenging to find this information quickly. + +The following is a directory of adopters to help identify users of individual features. The users themselves directly maintain the list. + +### Adding yourself as a user + +If you are using Zitadel, please consider adding yourself as a user with a quick description of your use case by opening a pull request to this file and adding a section describing your usage of Zitadel. | Organization/Individual | Contact Information | Description of Usage | | ----------------------- | -------------------------------------------------------- | ----------------------------------------------- | -| ZITADEL | [@fforootd](https://github.com/fforootd) (and many more) | ZITADEL Cloud makes heavy use of of ZITADEL ;-) | -| Organization Name | contact@example.com | Description of how they use ZITADEL | -| Individual Name | contact@example.com | Description of how they use ZITADEL | +| Zitadel | [@fforootd](https://github.com/fforootd) (and many more) | Zitadel Cloud makes heavy use of of Zitadel ;-) | +| Organization Name | contact@example.com | Description of how they use Zitadel | +| Individual Name | contact@example.com | Description of how they use Zitadel | From ccef67cefafe0bf239a3046558735e718b8d56b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 26 Nov 2024 17:26:41 +0200 Subject: [PATCH 30/32] fix(eventstore): cleanup org fields on remove (#8946) # Which Problems Are Solved When an org is removed, the corresponding fields are not deleted. This creates issues, such as recreating a new org with the same verified domain. # How the Problems Are Solved Remove the search fields by the org aggregate, instead of just setting the removed state. # Additional Changes - Cleanup migration script that removed current stale fields. # Additional Context - Closes https://github.com/zitadel/zitadel/issues/8943 - Related to https://github.com/zitadel/zitadel/pull/8790 --------- Co-authored-by: Silvan --- cmd/setup/39.go | 27 +++++++++++++++++++++++++++ cmd/setup/39.sql | 6 ++++++ cmd/setup/config.go | 1 + cmd/setup/setup.go | 2 ++ internal/repository/org/org.go | 18 +----------------- 5 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 cmd/setup/39.go create mode 100644 cmd/setup/39.sql diff --git a/cmd/setup/39.go b/cmd/setup/39.go new file mode 100644 index 0000000000..01b0ddfcbf --- /dev/null +++ b/cmd/setup/39.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 39.sql + deleteStaleOrgFields string +) + +type DeleteStaleOrgFields struct { + dbClient *database.DB +} + +func (mig *DeleteStaleOrgFields) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, deleteStaleOrgFields) + return err +} + +func (mig *DeleteStaleOrgFields) String() string { + return "39_delete_stale_org_fields" +} diff --git a/cmd/setup/39.sql b/cmd/setup/39.sql new file mode 100644 index 0000000000..4abc815c25 --- /dev/null +++ b/cmd/setup/39.sql @@ -0,0 +1,6 @@ +DELETE FROM eventstore.fields +WHERE aggregate_type = 'org' +AND aggregate_id NOT IN ( + SELECT id + FROM projections.orgs1 +); diff --git a/cmd/setup/config.go b/cmd/setup/config.go index f688a2a3a4..b0a143b698 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -125,6 +125,7 @@ type Steps struct { s36FillV2Milestones *FillV3Milestones s37Apps7OIDConfigsBackChannelLogoutURI *Apps7OIDConfigsBackChannelLogoutURI s38BackChannelLogoutNotificationStart *BackChannelLogoutNotificationStart + s39DeleteStaleOrgFields *DeleteStaleOrgFields } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index aeed1523b1..b8ea708cbf 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -169,6 +169,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s36FillV2Milestones = &FillV3Milestones{dbClient: queryDBClient, eventstore: eventstoreClient} steps.s37Apps7OIDConfigsBackChannelLogoutURI = &Apps7OIDConfigsBackChannelLogoutURI{dbClient: esPusherDBClient} steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: esPusherDBClient, esClient: eventstoreClient} + steps.s39DeleteStaleOrgFields = &DeleteStaleOrgFields{dbClient: esPusherDBClient} err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -232,6 +233,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s32AddAuthSessionID, steps.s33SMSConfigs3TwilioAddVerifyServiceSid, steps.s37Apps7OIDConfigsBackChannelLogoutURI, + steps.s39DeleteStaleOrgFields, } { mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") } diff --git a/internal/repository/org/org.go b/internal/repository/org/org.go index 95c9c2f3ed..cf8a3ce114 100644 --- a/internal/repository/org/org.go +++ b/internal/repository/org/org.go @@ -310,23 +310,7 @@ func (e *OrgRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { func (e *OrgRemovedEvent) Fields() []*eventstore.FieldOperation { // TODO: project grants are currently not removed because we don't have the relationship between the granted org and the grant return []*eventstore.FieldOperation{ - eventstore.SetField( - e.Aggregate(), - orgSearchObject(e.Aggregate().ID), - OrgStateSearchField, - &eventstore.Value{ - Value: domain.OrgStateRemoved, - ShouldIndex: true, - }, - - eventstore.FieldTypeInstanceID, - eventstore.FieldTypeResourceOwner, - eventstore.FieldTypeAggregateType, - eventstore.FieldTypeAggregateID, - eventstore.FieldTypeObjectType, - eventstore.FieldTypeObjectID, - eventstore.FieldTypeFieldName, - ), + eventstore.RemoveSearchFieldsByAggregate(e.Aggregate()), } } From 4413efd82c6392955474d1a4d4526ad30f73c0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 27 Nov 2024 16:32:13 +0200 Subject: [PATCH 31/32] chore: remove parallel running in integration tests (#8904) # Which Problems Are Solved Integration tests are flaky due to eventual consistency. # How the Problems Are Solved Remove t.Parallel so that less concurrent requests on multiple instance happen. This allows the projections to catch up more easily. # Additional Changes - none # Additional Context - none --- .../integration_test/iam_settings_test.go | 4 --- ...ons_allow_public_org_registrations_test.go | 2 -- .../restrictions_allowed_languages_test.go | 2 -- .../grpc/org/v2/integration_test/org_test.go | 2 -- .../org/v2/integration_test/query_test.go | 2 -- .../integration_test/execution_target_test.go | 1 - .../integration_test/execution_test.go | 5 --- .../v3alpha/integration_test/query_test.go | 3 -- .../v3alpha/integration_test/target_test.go | 1 - .../v3alpha/integration_test/email_test.go | 3 -- .../v3alpha/integration_test/phone_test.go | 3 -- .../v3alpha/integration_test/user_test.go | 7 ---- .../v3alpha/integration_test/query_test.go | 2 -- .../integration_test/userschema_test.go | 5 --- .../system/integration_test/instance_test.go | 2 -- .../limits_auditlogretention_test.go | 2 -- .../integration_test/limits_block_test.go | 2 -- .../user/v2/integration_test/email_test.go | 6 ---- .../user/v2/integration_test/idp_link_test.go | 6 ---- .../grpc/user/v2/integration_test/otp_test.go | 8 ----- .../user/v2/integration_test/passkey_test.go | 10 ------ .../user/v2/integration_test/password_test.go | 4 --- .../user/v2/integration_test/phone_test.go | 8 ----- .../user/v2/integration_test/query_test.go | 6 ---- .../user/v2/integration_test/totp_test.go | 6 ---- .../grpc/user/v2/integration_test/u2f_test.go | 6 ---- .../user/v2/integration_test/user_test.go | 32 +------------------ .../v2beta/integration_test/email_test.go | 6 ---- .../user/v2beta/integration_test/otp_test.go | 8 ----- .../v2beta/integration_test/passkey_test.go | 6 ---- .../v2beta/integration_test/password_test.go | 4 --- .../v2beta/integration_test/phone_test.go | 8 ----- .../v2beta/integration_test/query_test.go | 6 ---- .../user/v2beta/integration_test/totp_test.go | 6 ---- .../user/v2beta/integration_test/u2f_test.go | 4 --- .../user/v2beta/integration_test/user_test.go | 28 +--------------- .../integration_test/auth_request_test.go | 26 --------------- .../api/oidc/integration_test/client_test.go | 6 ---- .../api/oidc/integration_test/keys_test.go | 2 -- .../api/oidc/integration_test/server_test.go | 2 -- .../token_client_credentials_test.go | 2 -- .../integration_test/token_exchange_test.go | 6 ---- .../token_jwt_profile_test.go | 2 -- .../integration_test/telemetry_pusher_test.go | 2 -- 44 files changed, 2 insertions(+), 262 deletions(-) diff --git a/internal/api/grpc/admin/integration_test/iam_settings_test.go b/internal/api/grpc/admin/integration_test/iam_settings_test.go index 9eca09c06c..603d4a5d32 100644 --- a/internal/api/grpc/admin/integration_test/iam_settings_test.go +++ b/internal/api/grpc/admin/integration_test/iam_settings_test.go @@ -18,8 +18,6 @@ import ( ) func TestServer_GetSecurityPolicy(t *testing.T) { - t.Parallel() - instance := integration.NewInstance(CTX) adminCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -72,8 +70,6 @@ func TestServer_GetSecurityPolicy(t *testing.T) { } func TestServer_SetSecurityPolicy(t *testing.T) { - t.Parallel() - instance := integration.NewInstance(CTX) adminCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/admin/integration_test/restrictions_allow_public_org_registrations_test.go b/internal/api/grpc/admin/integration_test/restrictions_allow_public_org_registrations_test.go index 3cbcf8abd0..9aed3f5924 100644 --- a/internal/api/grpc/admin/integration_test/restrictions_allow_public_org_registrations_test.go +++ b/internal/api/grpc/admin/integration_test/restrictions_allow_public_org_registrations_test.go @@ -21,8 +21,6 @@ import ( ) func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) { - t.Parallel() - instance := integration.NewInstance(CTX) regOrgUrl, err := url.Parse("http://" + instance.Domain + ":8080/ui/login/register/org") require.NoError(t, err) diff --git a/internal/api/grpc/admin/integration_test/restrictions_allowed_languages_test.go b/internal/api/grpc/admin/integration_test/restrictions_allowed_languages_test.go index e00b7f221b..52a1607bba 100644 --- a/internal/api/grpc/admin/integration_test/restrictions_allowed_languages_test.go +++ b/internal/api/grpc/admin/integration_test/restrictions_allowed_languages_test.go @@ -24,8 +24,6 @@ import ( ) func TestServer_Restrictions_AllowedLanguages(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() diff --git a/internal/api/grpc/org/v2/integration_test/org_test.go b/internal/api/grpc/org/v2/integration_test/org_test.go index 165eb1471f..8de5e40bb5 100644 --- a/internal/api/grpc/org/v2/integration_test/org_test.go +++ b/internal/api/grpc/org/v2/integration_test/org_test.go @@ -42,8 +42,6 @@ func TestMain(m *testing.M) { } func TestServer_AddOrganization(t *testing.T) { - t.Parallel() - idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) tests := []struct { diff --git a/internal/api/grpc/org/v2/integration_test/query_test.go b/internal/api/grpc/org/v2/integration_test/query_test.go index bd0352ed75..188aeddf9f 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -27,8 +27,6 @@ type orgAttr struct { } func TestServer_ListOrganizations(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *org.ListOrganizationsRequest diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go index c70ed227c1..7aff6afb3f 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go +++ b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_target_test.go @@ -26,7 +26,6 @@ import ( ) func TestServer_ExecutionTarget(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go index 19d97c3857..b56efd6b99 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go +++ b/internal/api/grpc/resources/action/v3alpha/integration_test/execution_test.go @@ -25,7 +25,6 @@ func executionTargetsSingleInclude(include *action.Condition) []*action.Executio } func TestServer_SetExecution_Request(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -207,7 +206,6 @@ func TestServer_SetExecution_Request(t *testing.T) { } func TestServer_SetExecution_Request_Include(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -346,7 +344,6 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { } func TestServer_SetExecution_Response(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -528,7 +525,6 @@ func TestServer_SetExecution_Response(t *testing.T) { } func TestServer_SetExecution_Event(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -716,7 +712,6 @@ func TestServer_SetExecution_Event(t *testing.T) { } func TestServer_SetExecution_Function(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go index d6f584b35e..23fb860cd3 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go +++ b/internal/api/grpc/resources/action/v3alpha/integration_test/query_test.go @@ -22,7 +22,6 @@ import ( ) func TestServer_GetTarget(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -237,7 +236,6 @@ func TestServer_GetTarget(t *testing.T) { } func TestServer_ListTargets(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -503,7 +501,6 @@ func TestServer_ListTargets(t *testing.T) { } func TestServer_SearchExecutions(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go b/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go index 60c4ac35a7..04fa60982d 100644 --- a/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go +++ b/internal/api/grpc/resources/action/v3alpha/integration_test/target_test.go @@ -21,7 +21,6 @@ import ( ) func TestServer_CreateTarget(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/resources/user/v3alpha/integration_test/email_test.go b/internal/api/grpc/resources/user/v3alpha/integration_test/email_test.go index 4b5a342905..f64bcebe38 100644 --- a/internal/api/grpc/resources/user/v3alpha/integration_test/email_test.go +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/email_test.go @@ -19,7 +19,6 @@ import ( ) func TestServer_SetContactEmail(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -365,7 +364,6 @@ func TestServer_SetContactEmail(t *testing.T) { } func TestServer_VerifyContactEmail(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -555,7 +553,6 @@ func TestServer_VerifyContactEmail(t *testing.T) { } func TestServer_ResendContactEmailCode(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/resources/user/v3alpha/integration_test/phone_test.go b/internal/api/grpc/resources/user/v3alpha/integration_test/phone_test.go index 9fcacd7457..fbd5805f16 100644 --- a/internal/api/grpc/resources/user/v3alpha/integration_test/phone_test.go +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/phone_test.go @@ -18,7 +18,6 @@ import ( ) func TestServer_SetContactPhone(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -292,7 +291,6 @@ func TestServer_SetContactPhone(t *testing.T) { } func TestServer_VerifyContactPhone(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -484,7 +482,6 @@ func TestServer_VerifyContactPhone(t *testing.T) { } func TestServer_ResendContactPhoneCode(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go b/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go index 34d12446c5..1bc35a5390 100644 --- a/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go @@ -21,7 +21,6 @@ import ( ) func TestServer_CreateUser(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -230,7 +229,6 @@ func TestServer_CreateUser(t *testing.T) { } func TestServer_PatchUser(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -650,7 +648,6 @@ func TestServer_PatchUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -868,7 +865,6 @@ func unmarshalJSON(data string) *structpb.Struct { } func TestServer_LockUser(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -1070,7 +1066,6 @@ func TestServer_LockUser(t *testing.T) { } func TestServer_UnlockUser(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -1253,7 +1248,6 @@ func TestServer_UnlockUser(t *testing.T) { } func TestServer_DeactivateUser(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -1455,7 +1449,6 @@ func TestServer_DeactivateUser(t *testing.T) { } func TestServer_ActivateUser(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/resources/userschema/v3alpha/integration_test/query_test.go b/internal/api/grpc/resources/userschema/v3alpha/integration_test/query_test.go index ef7fe02807..31ee68ed8d 100644 --- a/internal/api/grpc/resources/userschema/v3alpha/integration_test/query_test.go +++ b/internal/api/grpc/resources/userschema/v3alpha/integration_test/query_test.go @@ -20,7 +20,6 @@ import ( ) func TestServer_ListUserSchemas(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -214,7 +213,6 @@ func TestServer_ListUserSchemas(t *testing.T) { } func TestServer_GetUserSchema(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/resources/userschema/v3alpha/integration_test/userschema_test.go b/internal/api/grpc/resources/userschema/v3alpha/integration_test/userschema_test.go index a264b163eb..33b4565861 100644 --- a/internal/api/grpc/resources/userschema/v3alpha/integration_test/userschema_test.go +++ b/internal/api/grpc/resources/userschema/v3alpha/integration_test/userschema_test.go @@ -19,7 +19,6 @@ import ( ) func TestServer_CreateUserSchema(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -304,7 +303,6 @@ func TestServer_CreateUserSchema(t *testing.T) { } func TestServer_UpdateUserSchema(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -596,7 +594,6 @@ func TestServer_UpdateUserSchema(t *testing.T) { } func TestServer_DeactivateUserSchema(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -678,7 +675,6 @@ func TestServer_DeactivateUserSchema(t *testing.T) { } func TestServer_ReactivateUserSchema(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -760,7 +756,6 @@ func TestServer_ReactivateUserSchema(t *testing.T) { } func TestServer_DeleteUserSchema(t *testing.T) { - t.Parallel() instance := integration.NewInstance(CTX) ensureFeatureEnabled(t, instance) isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/grpc/system/integration_test/instance_test.go b/internal/api/grpc/system/integration_test/instance_test.go index 18d2f68bb9..2c8cc8cf89 100644 --- a/internal/api/grpc/system/integration_test/instance_test.go +++ b/internal/api/grpc/system/integration_test/instance_test.go @@ -15,8 +15,6 @@ import ( ) func TestServer_ListInstances(t *testing.T) { - t.Parallel() - isoInstance := integration.NewInstance(CTX) tests := []struct { diff --git a/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go b/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go index 077054eb33..24c224b0fe 100644 --- a/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go +++ b/internal/api/grpc/system/integration_test/limits_auditlogretention_test.go @@ -22,8 +22,6 @@ import ( ) func TestServer_Limits_AuditLogRetention(t *testing.T) { - t.Parallel() - isoInstance := integration.NewInstance(CTX) iamOwnerCtx := isoInstance.WithAuthorization(CTX, integration.UserTypeIAMOwner) userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t, isoInstance.Client) diff --git a/internal/api/grpc/system/integration_test/limits_block_test.go b/internal/api/grpc/system/integration_test/limits_block_test.go index 46b213f603..d3b9fffbd3 100644 --- a/internal/api/grpc/system/integration_test/limits_block_test.go +++ b/internal/api/grpc/system/integration_test/limits_block_test.go @@ -26,8 +26,6 @@ import ( ) func TestServer_Limits_Block(t *testing.T) { - t.Parallel() - isoInstance := integration.NewInstance(CTX) iamOwnerCtx := isoInstance.WithAuthorization(CTX, integration.UserTypeIAMOwner) tests := []*test{ diff --git a/internal/api/grpc/user/v2/integration_test/email_test.go b/internal/api/grpc/user/v2/integration_test/email_test.go index 5092dbf40d..37d575016b 100644 --- a/internal/api/grpc/user/v2/integration_test/email_test.go +++ b/internal/api/grpc/user/v2/integration_test/email_test.go @@ -17,8 +17,6 @@ import ( ) func TestServer_SetEmail(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -148,8 +146,6 @@ func TestServer_SetEmail(t *testing.T) { } func TestServer_ResendEmailCode(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() @@ -254,8 +250,6 @@ func TestServer_ResendEmailCode(t *testing.T) { } func TestServer_VerifyEmail(t *testing.T) { - t.Parallel() - userResp := Instance.CreateHumanUser(CTX) tests := []struct { name string diff --git a/internal/api/grpc/user/v2/integration_test/idp_link_test.go b/internal/api/grpc/user/v2/integration_test/idp_link_test.go index ab398c7233..116a095216 100644 --- a/internal/api/grpc/user/v2/integration_test/idp_link_test.go +++ b/internal/api/grpc/user/v2/integration_test/idp_link_test.go @@ -20,8 +20,6 @@ import ( ) func TestServer_AddIDPLink(t *testing.T) { - t.Parallel() - idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -101,8 +99,6 @@ func TestServer_AddIDPLink(t *testing.T) { } func TestServer_ListIDPLinks(t *testing.T) { - t.Parallel() - orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks-%s", gofakeit.AppName()), gofakeit.Email()) instanceIdpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) @@ -257,8 +253,6 @@ func TestServer_ListIDPLinks(t *testing.T) { } func TestServer_RemoveIDPLink(t *testing.T) { - t.Parallel() - orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks-%s", gofakeit.AppName()), gofakeit.Email()) instanceIdpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) diff --git a/internal/api/grpc/user/v2/integration_test/otp_test.go b/internal/api/grpc/user/v2/integration_test/otp_test.go index 3070a1d584..ae7c040427 100644 --- a/internal/api/grpc/user/v2/integration_test/otp_test.go +++ b/internal/api/grpc/user/v2/integration_test/otp_test.go @@ -15,8 +15,6 @@ import ( ) func TestServer_AddOTPSMS(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -123,8 +121,6 @@ func TestServer_AddOTPSMS(t *testing.T) { } func TestServer_RemoveOTPSMS(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -191,8 +187,6 @@ func TestServer_RemoveOTPSMS(t *testing.T) { } func TestServer_AddOTPEmail(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -301,8 +295,6 @@ func TestServer_AddOTPEmail(t *testing.T) { } func TestServer_RemoveOTPEmail(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2/integration_test/passkey_test.go b/internal/api/grpc/user/v2/integration_test/passkey_test.go index 9d9cb8a047..881ab17c09 100644 --- a/internal/api/grpc/user/v2/integration_test/passkey_test.go +++ b/internal/api/grpc/user/v2/integration_test/passkey_test.go @@ -19,8 +19,6 @@ import ( ) func TestServer_RegisterPasskey(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ UserId: userID, @@ -141,8 +139,6 @@ func TestServer_RegisterPasskey(t *testing.T) { } func TestServer_VerifyPasskeyRegistration(t *testing.T) { - t.Parallel() - userID, pkr := userWithPasskeyRegistered(t) attestationResponse, err := Instance.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) @@ -219,8 +215,6 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) { } func TestServer_CreatePasskeyRegistrationLink(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() type args struct { @@ -354,8 +348,6 @@ func passkeyVerify(t *testing.T, userID string, pkr *user.RegisterPasskeyRespons } func TestServer_RemovePasskey(t *testing.T) { - t.Parallel() - userIDWithout := Instance.CreateHumanUser(CTX).GetUserId() userIDRegistered, pkrRegistered := userWithPasskeyRegistered(t) userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t) @@ -461,8 +453,6 @@ func TestServer_RemovePasskey(t *testing.T) { } func TestServer_ListPasskeys(t *testing.T) { - t.Parallel() - userIDWithout := Instance.CreateHumanUser(CTX).GetUserId() userIDRegistered, _ := userWithPasskeyRegistered(t) userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t) diff --git a/internal/api/grpc/user/v2/integration_test/password_test.go b/internal/api/grpc/user/v2/integration_test/password_test.go index 7707537653..0cd0da7454 100644 --- a/internal/api/grpc/user/v2/integration_test/password_test.go +++ b/internal/api/grpc/user/v2/integration_test/password_test.go @@ -17,8 +17,6 @@ import ( ) func TestServer_RequestPasswordReset(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -107,8 +105,6 @@ func TestServer_RequestPasswordReset(t *testing.T) { } func TestServer_SetPassword(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.SetPasswordRequest diff --git a/internal/api/grpc/user/v2/integration_test/phone_test.go b/internal/api/grpc/user/v2/integration_test/phone_test.go index 47590b6d67..1c1f75854d 100644 --- a/internal/api/grpc/user/v2/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2/integration_test/phone_test.go @@ -18,8 +18,6 @@ import ( ) func TestServer_SetPhone(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -124,8 +122,6 @@ func TestServer_SetPhone(t *testing.T) { } func TestServer_ResendPhoneCode(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() @@ -201,8 +197,6 @@ func TestServer_ResendPhoneCode(t *testing.T) { } func TestServer_VerifyPhone(t *testing.T) { - t.Parallel() - userResp := Instance.CreateHumanUser(CTX) tests := []struct { name string @@ -256,8 +250,6 @@ func TestServer_VerifyPhone(t *testing.T) { } func TestServer_RemovePhone(t *testing.T) { - t.Parallel() - userResp := Instance.CreateHumanUser(CTX) failResp := Instance.CreateHumanUserNoPhone(CTX) otherUser := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2/integration_test/query_test.go b/internal/api/grpc/user/v2/integration_test/query_test.go index 3d5b2d9416..a00d1b1a48 100644 --- a/internal/api/grpc/user/v2/integration_test/query_test.go +++ b/internal/api/grpc/user/v2/integration_test/query_test.go @@ -20,8 +20,6 @@ import ( ) func TestServer_GetUserByID(t *testing.T) { - t.Parallel() - orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { ctx context.Context @@ -188,8 +186,6 @@ func TestServer_GetUserByID(t *testing.T) { } func TestServer_GetUserByID_Permission(t *testing.T) { - t.Parallel() - newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() @@ -337,8 +333,6 @@ type userAttr struct { } func TestServer_ListUsers(t *testing.T) { - t.Parallel() - orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) userResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email()) type args struct { diff --git a/internal/api/grpc/user/v2/integration_test/totp_test.go b/internal/api/grpc/user/v2/integration_test/totp_test.go index add67876ff..e65756c1c1 100644 --- a/internal/api/grpc/user/v2/integration_test/totp_test.go +++ b/internal/api/grpc/user/v2/integration_test/totp_test.go @@ -18,8 +18,6 @@ import ( ) func TestServer_RegisterTOTP(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -106,8 +104,6 @@ func TestServer_RegisterTOTP(t *testing.T) { } func TestServer_VerifyTOTPRegistration(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -211,8 +207,6 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { } func TestServer_RemoveTOTP(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2/integration_test/u2f_test.go b/internal/api/grpc/user/v2/integration_test/u2f_test.go index 9ec0011249..b8af753f85 100644 --- a/internal/api/grpc/user/v2/integration_test/u2f_test.go +++ b/internal/api/grpc/user/v2/integration_test/u2f_test.go @@ -17,8 +17,6 @@ import ( ) func TestServer_RegisterU2F(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() otherUser := Instance.CreateHumanUser(CTX).GetUserId() @@ -108,8 +106,6 @@ func TestServer_RegisterU2F(t *testing.T) { } func TestServer_VerifyU2FRegistration(t *testing.T) { - t.Parallel() - ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t) attestationResponse, err := Instance.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) @@ -215,8 +211,6 @@ func ctxFromNewUserWithVerifiedU2F(t *testing.T) (context.Context, string, strin } func TestServer_RemoveU2F(t *testing.T) { - t.Parallel() - userIDWithout := Instance.CreateHumanUser(CTX).GetUserId() ctxRegistered, userIDRegistered, pkrRegistered := ctxFromNewUserWithRegisteredU2F(t) _, userIDVerified, u2fVerified := ctxFromNewUserWithVerifiedU2F(t) 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 e4196c1a30..cf42e9291f 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -53,8 +53,6 @@ func TestMain(m *testing.M) { } func TestServer_AddHumanUser(t *testing.T) { - t.Parallel() - idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -681,8 +679,6 @@ func TestServer_AddHumanUser(t *testing.T) { } func TestServer_AddHumanUser_Permission(t *testing.T) { - t.Parallel() - newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) type args struct { @@ -876,8 +872,6 @@ func TestServer_AddHumanUser_Permission(t *testing.T) { } func TestServer_UpdateHumanUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.UpdateHumanUserRequest @@ -1239,8 +1233,6 @@ func TestServer_UpdateHumanUser(t *testing.T) { } func TestServer_UpdateHumanUser_Permission(t *testing.T) { - t.Parallel() - newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() @@ -1324,8 +1316,6 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) { } func TestServer_LockUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.LockUserRequest @@ -1434,8 +1424,6 @@ func TestServer_LockUser(t *testing.T) { } func TestServer_UnLockUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.UnlockUserRequest @@ -1544,8 +1532,6 @@ func TestServer_UnLockUser(t *testing.T) { } func TestServer_DeactivateUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.DeactivateUserRequest @@ -1655,8 +1641,6 @@ func TestServer_DeactivateUser(t *testing.T) { } func TestServer_ReactivateUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.ReactivateUserRequest @@ -1765,8 +1749,6 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - t.Parallel() - projectResp, err := Instance.CreateProject(CTX) require.NoError(t, err) type args struct { @@ -1866,8 +1848,6 @@ func TestServer_DeleteUser(t *testing.T) { } func TestServer_StartIdentityProviderIntent(t *testing.T) { - t.Parallel() - idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) orgIdpResp := Instance.AddOrgGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("NotDefaultOrg-%s", gofakeit.AppName()), gofakeit.Email()) @@ -2131,9 +2111,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { /* func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - t.Parallel() - - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(t, CTX) intentID := Instance.CreateIntent(t, CTX, idpID) successfulID, token, changeDate, sequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", "id") successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "user", "id") @@ -2421,8 +2399,6 @@ func ctxFromNewUserWithVerifiedPasswordlessLegacy(t *testing.T) (context.Context } func TestServer_ListAuthenticationMethodTypes(t *testing.T) { - t.Parallel() - userIDWithoutAuth := Instance.CreateHumanUser(CTX).GetUserId() userIDWithPasskey := Instance.CreateHumanUser(CTX).GetUserId() @@ -2654,8 +2630,6 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { } func TestServer_CreateInviteCode(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.CreateInviteCodeRequest @@ -2787,8 +2761,6 @@ func TestServer_CreateInviteCode(t *testing.T) { } func TestServer_ResendInviteCode(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.ResendInviteCodeRequest @@ -2878,8 +2850,6 @@ func TestServer_ResendInviteCode(t *testing.T) { } func TestServer_VerifyInviteCode(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.VerifyInviteCodeRequest diff --git a/internal/api/grpc/user/v2beta/integration_test/email_test.go b/internal/api/grpc/user/v2beta/integration_test/email_test.go index ebace6daa2..d22355978a 100644 --- a/internal/api/grpc/user/v2beta/integration_test/email_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/email_test.go @@ -17,8 +17,6 @@ import ( ) func TestServer_SetEmail(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -148,8 +146,6 @@ func TestServer_SetEmail(t *testing.T) { } func TestServer_ResendEmailCode(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() @@ -254,8 +250,6 @@ func TestServer_ResendEmailCode(t *testing.T) { } func TestServer_VerifyEmail(t *testing.T) { - t.Parallel() - userResp := Instance.CreateHumanUser(CTX) tests := []struct { name string diff --git a/internal/api/grpc/user/v2beta/integration_test/otp_test.go b/internal/api/grpc/user/v2beta/integration_test/otp_test.go index 3ffec36ef0..6d6e2eff3e 100644 --- a/internal/api/grpc/user/v2beta/integration_test/otp_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/otp_test.go @@ -15,8 +15,6 @@ import ( ) func TestServer_AddOTPSMS(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -123,8 +121,6 @@ func TestServer_AddOTPSMS(t *testing.T) { } func TestServer_RemoveOTPSMS(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -191,8 +187,6 @@ func TestServer_RemoveOTPSMS(t *testing.T) { } func TestServer_AddOTPEmail(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -301,8 +295,6 @@ func TestServer_AddOTPEmail(t *testing.T) { } func TestServer_RemoveOTPEmail(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2beta/integration_test/passkey_test.go b/internal/api/grpc/user/v2beta/integration_test/passkey_test.go index e5a0ec193b..acca01885c 100644 --- a/internal/api/grpc/user/v2beta/integration_test/passkey_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/passkey_test.go @@ -18,8 +18,6 @@ import ( ) func TestServer_RegisterPasskey(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ UserId: userID, @@ -140,8 +138,6 @@ func TestServer_RegisterPasskey(t *testing.T) { } func TestServer_VerifyPasskeyRegistration(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ UserId: userID, @@ -230,8 +226,6 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) { } func TestServer_CreatePasskeyRegistrationLink(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() type args struct { diff --git a/internal/api/grpc/user/v2beta/integration_test/password_test.go b/internal/api/grpc/user/v2beta/integration_test/password_test.go index 5995f87c7f..fa6bc66104 100644 --- a/internal/api/grpc/user/v2beta/integration_test/password_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/password_test.go @@ -17,8 +17,6 @@ import ( ) func TestServer_RequestPasswordReset(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -109,8 +107,6 @@ func TestServer_RequestPasswordReset(t *testing.T) { } func TestServer_SetPassword(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.SetPasswordRequest diff --git a/internal/api/grpc/user/v2beta/integration_test/phone_test.go b/internal/api/grpc/user/v2beta/integration_test/phone_test.go index 03567f4023..cd7199dcea 100644 --- a/internal/api/grpc/user/v2beta/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/phone_test.go @@ -18,8 +18,6 @@ import ( ) func TestServer_SetPhone(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -126,8 +124,6 @@ func TestServer_SetPhone(t *testing.T) { } func TestServer_ResendPhoneCode(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() @@ -204,8 +200,6 @@ func TestServer_ResendPhoneCode(t *testing.T) { } func TestServer_VerifyPhone(t *testing.T) { - t.Parallel() - userResp := Instance.CreateHumanUser(CTX) tests := []struct { name string @@ -258,8 +252,6 @@ func TestServer_VerifyPhone(t *testing.T) { } func TestServer_RemovePhone(t *testing.T) { - t.Parallel() - userResp := Instance.CreateHumanUser(CTX) failResp := Instance.CreateHumanUserNoPhone(CTX) otherUser := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2beta/integration_test/query_test.go b/internal/api/grpc/user/v2beta/integration_test/query_test.go index 654a84a5d4..fc1d71926e 100644 --- a/internal/api/grpc/user/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/query_test.go @@ -29,8 +29,6 @@ func detailsV2ToV2beta(obj *object.Details) *object_v2beta.Details { } func TestServer_GetUserByID(t *testing.T) { - t.Parallel() - orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { ctx context.Context @@ -197,8 +195,6 @@ func TestServer_GetUserByID(t *testing.T) { } func TestServer_GetUserByID_Permission(t *testing.T) { - t.Parallel() - timeNow := time.Now().UTC() newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) @@ -347,8 +343,6 @@ type userAttr struct { } func TestServer_ListUsers(t *testing.T) { - t.Parallel() - orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) userResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email()) type args struct { diff --git a/internal/api/grpc/user/v2beta/integration_test/totp_test.go b/internal/api/grpc/user/v2beta/integration_test/totp_test.go index 9fcce47321..4afe5e1f31 100644 --- a/internal/api/grpc/user/v2beta/integration_test/totp_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/totp_test.go @@ -18,8 +18,6 @@ import ( ) func TestServer_RegisterTOTP(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -106,8 +104,6 @@ func TestServer_RegisterTOTP(t *testing.T) { } func TestServer_VerifyTOTPRegistration(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -216,8 +212,6 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { } func TestServer_RemoveTOTP(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2beta/integration_test/u2f_test.go b/internal/api/grpc/user/v2beta/integration_test/u2f_test.go index c35231eefc..6e47cbbb99 100644 --- a/internal/api/grpc/user/v2beta/integration_test/u2f_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/u2f_test.go @@ -17,8 +17,6 @@ import ( ) func TestServer_RegisterU2F(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() otherUser := Instance.CreateHumanUser(CTX).GetUserId() @@ -108,8 +106,6 @@ func TestServer_RegisterU2F(t *testing.T) { } func TestServer_VerifyU2FRegistration(t *testing.T) { - t.Parallel() - userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) 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 b5f0b16d20..2e0abbb6b9 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -51,8 +51,6 @@ func TestMain(m *testing.M) { } func TestServer_AddHumanUser(t *testing.T) { - t.Parallel() - idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -638,8 +636,6 @@ func TestServer_AddHumanUser(t *testing.T) { } func TestServer_AddHumanUser_Permission(t *testing.T) { - t.Parallel() - newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), gofakeit.Email()) type args struct { ctx context.Context @@ -832,8 +828,6 @@ func TestServer_AddHumanUser_Permission(t *testing.T) { } func TestServer_UpdateHumanUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.UpdateHumanUserRequest @@ -1195,8 +1189,6 @@ func TestServer_UpdateHumanUser(t *testing.T) { } func TestServer_UpdateHumanUser_Permission(t *testing.T) { - t.Parallel() - newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman-%s", gofakeit.AppName()), gofakeit.Email()) newUserID := newOrg.CreatedAdmins[0].GetUserId() type args struct { @@ -1279,8 +1271,6 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) { } func TestServer_LockUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.LockUserRequest @@ -1389,8 +1379,6 @@ func TestServer_LockUser(t *testing.T) { } func TestServer_UnLockUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.UnlockUserRequest @@ -1499,8 +1487,6 @@ func TestServer_UnLockUser(t *testing.T) { } func TestServer_DeactivateUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.DeactivateUserRequest @@ -1609,8 +1595,6 @@ func TestServer_DeactivateUser(t *testing.T) { } func TestServer_ReactivateUser(t *testing.T) { - t.Parallel() - type args struct { ctx context.Context req *user.ReactivateUserRequest @@ -1719,8 +1703,6 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - t.Parallel() - projectResp, err := Instance.CreateProject(CTX) require.NoError(t, err) type args struct { @@ -1820,8 +1802,6 @@ func TestServer_DeleteUser(t *testing.T) { } func TestServer_AddIDPLink(t *testing.T) { - t.Parallel() - idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -1901,8 +1881,6 @@ func TestServer_AddIDPLink(t *testing.T) { } func TestServer_StartIdentityProviderIntent(t *testing.T) { - t.Parallel() - idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) orgIdpID := Instance.AddOrgGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("NotDefaultOrg-%s", gofakeit.AppName()), gofakeit.Email()) @@ -2166,9 +2144,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { /* func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - t.Parallel() - - idpID := Instance.AddGenericOAuthProvider(t, CTX) + idpID := Instance.AddGenericOAuthProvider(t, CTX) intentID := Instance.CreateIntent(t, CTX, idpID) successfulID, token, changeDate, sequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID.Id, "", "id") successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID.Id, "user", "id") @@ -2428,8 +2404,6 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { */ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { - t.Parallel() - userIDWithoutAuth := Instance.CreateHumanUser(CTX).GetUserId() userIDWithPasskey := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/oidc/integration_test/auth_request_test.go b/internal/api/oidc/integration_test/auth_request_test.go index bd9142a3f6..7ac0e24694 100644 --- a/internal/api/oidc/integration_test/auth_request_test.go +++ b/internal/api/oidc/integration_test/auth_request_test.go @@ -28,8 +28,6 @@ var ( ) func TestOPStorage_CreateAuthRequest(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) id := createAuthRequest(t, Instance, clientID, redirectURI) @@ -37,8 +35,6 @@ func TestOPStorage_CreateAuthRequest(t *testing.T) { } func TestOPStorage_CreateAccessToken_code(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) authRequestID := createAuthRequest(t, Instance, clientID, redirectURI) sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -78,8 +74,6 @@ func TestOPStorage_CreateAccessToken_code(t *testing.T) { } func TestOPStorage_CreateAccessToken_implicit(t *testing.T) { - t.Parallel() - clientID := createImplicitClient(t) authRequestID := createAuthRequestImplicit(t, clientID, redirectURIImplicit) sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -130,8 +124,6 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) { } func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) authRequestID := createAuthRequest(t, Instance, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -155,8 +147,6 @@ func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) { } func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) @@ -193,8 +183,6 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) { } func TestOPStorage_RevokeToken_access_token(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) @@ -238,8 +226,6 @@ func TestOPStorage_RevokeToken_access_token(t *testing.T) { } func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) @@ -277,8 +263,6 @@ func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T } func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) @@ -322,8 +306,6 @@ func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { } func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) @@ -361,8 +343,6 @@ func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing. } func TestOPStorage_RevokeToken_invalid_client(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) authRequestID := createAuthRequest(t, Instance, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -393,8 +373,6 @@ func TestOPStorage_RevokeToken_invalid_client(t *testing.T) { } func TestOPStorage_TerminateSession(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) @@ -432,8 +410,6 @@ func TestOPStorage_TerminateSession(t *testing.T) { } func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) @@ -478,8 +454,6 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { } func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/client_test.go b/internal/api/oidc/integration_test/client_test.go index 1b9ccd5cb3..6d61e84437 100644 --- a/internal/api/oidc/integration_test/client_test.go +++ b/internal/api/oidc/integration_test/client_test.go @@ -24,8 +24,6 @@ import ( ) func TestServer_Introspect(t *testing.T) { - t.Parallel() - project, err := Instance.CreateProject(CTX) require.NoError(t, err) app, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) @@ -143,8 +141,6 @@ func TestServer_Introspect(t *testing.T) { } func TestServer_Introspect_invalid_auth_invalid_token(t *testing.T) { - t.Parallel() - // ensure that when an invalid authentication and token is sent, the authentication error is returned // https://github.com/zitadel/zitadel/pull/8133 resourceServer, err := Instance.CreateResourceServerClientCredentials(CTX, "xxxxx", "xxxxx") @@ -191,8 +187,6 @@ func assertIntrospection( // TestServer_VerifyClient tests verification by running code flow tests // with clients that have different authentication methods. func TestServer_VerifyClient(t *testing.T) { - t.Parallel() - sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) project, err := Instance.CreateProject(CTX) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/keys_test.go b/internal/api/oidc/integration_test/keys_test.go index e8160017a5..8b66e980d0 100644 --- a/internal/api/oidc/integration_test/keys_test.go +++ b/internal/api/oidc/integration_test/keys_test.go @@ -24,8 +24,6 @@ import ( ) func TestServer_Keys(t *testing.T) { - t.Parallel() - instance := integration.NewInstance(CTX) ctxLogin := instance.WithAuthorization(CTX, integration.UserTypeLogin) diff --git a/internal/api/oidc/integration_test/server_test.go b/internal/api/oidc/integration_test/server_test.go index fcf9bfb65e..ea2fa0e2ee 100644 --- a/internal/api/oidc/integration_test/server_test.go +++ b/internal/api/oidc/integration_test/server_test.go @@ -18,8 +18,6 @@ import ( ) func TestServer_RefreshToken_Status(t *testing.T) { - t.Parallel() - clientID, _ := createClient(t, Instance) provider, err := Instance.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/token_client_credentials_test.go b/internal/api/oidc/integration_test/token_client_credentials_test.go index d43f40e53e..0567b4ce78 100644 --- a/internal/api/oidc/integration_test/token_client_credentials_test.go +++ b/internal/api/oidc/integration_test/token_client_credentials_test.go @@ -22,8 +22,6 @@ import ( ) func TestServer_ClientCredentialsExchange(t *testing.T) { - t.Parallel() - machine, name, clientID, clientSecret, err := Instance.CreateOIDCCredentialsClient(CTX) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/token_exchange_test.go b/internal/api/oidc/integration_test/token_exchange_test.go index 5b0b86f0ec..1319eea19a 100644 --- a/internal/api/oidc/integration_test/token_exchange_test.go +++ b/internal/api/oidc/integration_test/token_exchange_test.go @@ -143,8 +143,6 @@ func refreshTokenVerifier(ctx context.Context, provider rp.RelyingParty, subject } func TestServer_TokenExchange(t *testing.T) { - t.Parallel() - instance := integration.NewInstance(CTX) ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) userResp := instance.CreateHumanUser(ctx) @@ -365,8 +363,6 @@ func TestServer_TokenExchange(t *testing.T) { } func TestServer_TokenExchangeImpersonation(t *testing.T) { - t.Parallel() - instance := integration.NewInstance(CTX) ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) userResp := instance.CreateHumanUser(ctx) @@ -581,8 +577,6 @@ func TestServer_TokenExchangeImpersonation(t *testing.T) { // This test tries to call the zitadel API with an impersonated token, // which should fail. func TestImpersonation_API_Call(t *testing.T) { - t.Parallel() - instance := integration.NewInstance(CTX) ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) diff --git a/internal/api/oidc/integration_test/token_jwt_profile_test.go b/internal/api/oidc/integration_test/token_jwt_profile_test.go index ac483cf620..5845713317 100644 --- a/internal/api/oidc/integration_test/token_jwt_profile_test.go +++ b/internal/api/oidc/integration_test/token_jwt_profile_test.go @@ -21,8 +21,6 @@ import ( ) func TestServer_JWTProfile(t *testing.T) { - t.Parallel() - user, name, keyData, err := Instance.CreateOIDCJWTProfileClient(CTX) require.NoError(t, err) diff --git a/internal/notification/handlers/integration_test/telemetry_pusher_test.go b/internal/notification/handlers/integration_test/telemetry_pusher_test.go index 6b4ac10258..0779df7b34 100644 --- a/internal/notification/handlers/integration_test/telemetry_pusher_test.go +++ b/internal/notification/handlers/integration_test/telemetry_pusher_test.go @@ -26,8 +26,6 @@ import ( ) func TestServer_TelemetryPushMilestones(t *testing.T) { - t.Parallel() - sub := sink.Subscribe(CTX, sink.ChannelMilestone) defer sub.Close() From 8537805ea548452c9a88165d9882b0e67c1e9b93 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 27 Nov 2024 16:01:17 +0100 Subject: [PATCH 32/32] feat(notification): use event worker pool (#8962) # Which Problems Are Solved The current handling of notification follows the same pattern as all other projections: Created events are handled sequentially (based on "position") by a handler. During the process, a lot of information is aggregated (user, texts, templates, ...). This leads to back pressure on the projection since the handling of events might take longer than the time before a new event (to be handled) is created. # How the Problems Are Solved - The current user notification handler creates separate notification events based on the user / session events. - These events contain all the present and required information including the userID. - These notification events get processed by notification workers, which gather the necessary information (recipient address, texts, templates) to send out these notifications. - If a notification fails, a retry event is created based on the current notification request including the current state of the user (this prevents race conditions, where a user is changed in the meantime and the notification already gets the new state). - The retry event will be handled after a backoff delay. This delay increases with every attempt. - If the configured amount of attempts is reached or the message expired (based on config), a cancel event is created, letting the workers know, the notification must no longer be handled. - In case of successful send, a sent event is created for the notification aggregate and the existing "sent" events for the user / session object is stored. - The following is added to the defaults.yaml to allow configuration of the notification workers: ```yaml Notifications: # The amount of workers processing the notification request events. # If set to 0, no notification request events will be handled. This can be useful when running in # multi binary / pod setup and allowing only certain executables to process the events. Workers: 1 # ZITADEL_NOTIFIACATIONS_WORKERS # The amount of events a single worker will process in a run. BulkLimit: 10 # ZITADEL_NOTIFIACATIONS_BULKLIMIT # Time interval between scheduled notifications for request events RequeueEvery: 2s # ZITADEL_NOTIFIACATIONS_REQUEUEEVERY # The amount of workers processing the notification retry events. # If set to 0, no notification retry events will be handled. This can be useful when running in # multi binary / pod setup and allowing only certain executables to process the events. RetryWorkers: 1 # ZITADEL_NOTIFIACATIONS_RETRYWORKERS # Time interval between scheduled notifications for retry events RetryRequeueEvery: 2s # ZITADEL_NOTIFIACATIONS_RETRYREQUEUEEVERY # Only instances are projected, for which at least a projection-relevant event exists within the timeframe # from HandleActiveInstances duration in the past until the projection's current time # If set to 0 (default), every instance is always considered active HandleActiveInstances: 0s # ZITADEL_NOTIFIACATIONS_HANDLEACTIVEINSTANCES # The maximum duration a transaction remains open # before it spots left folding additional events # and updates the table. TransactionDuration: 1m # ZITADEL_NOTIFIACATIONS_TRANSACTIONDURATION # Automatically cancel the notification after the amount of failed attempts MaxAttempts: 3 # ZITADEL_NOTIFIACATIONS_MAXATTEMPTS # Automatically cancel the notification if it cannot be handled within a specific time MaxTtl: 5m # ZITADEL_NOTIFIACATIONS_MAXTTL # Failed attempts are retried after a confogired delay (with exponential backoff). # Set a minimum and maximum delay and a factor for the backoff MinRetryDelay: 1s # ZITADEL_NOTIFIACATIONS_MINRETRYDELAY MaxRetryDelay: 20s # ZITADEL_NOTIFIACATIONS_MAXRETRYDELAY # Any factor below 1 will be set to 1 RetryDelayFactor: 1.5 # ZITADEL_NOTIFIACATIONS_RETRYDELAYFACTOR ``` # Additional Changes None # Additional Context - closes #8931 --- .github/workflows/build.yml | 2 +- cmd/defaults.yaml | 34 + cmd/mirror/projections.go | 3 + cmd/setup/config.go | 1 + cmd/setup/setup.go | 2 + cmd/start/config.go | 1 + cmd/start/start.go | 2 + .../api/ui/login/init_password_handler.go | 16 +- internal/api/ui/login/init_user_handler.go | 20 +- internal/api/ui/login/invite_user_handler.go | 18 +- internal/api/ui/login/mail_verify_handler.go | 16 +- .../api/ui/login/mfa_verify_otp_handler.go | 4 +- internal/command/notification.go | 162 + internal/domain/human_web_auth_n.go | 4 + internal/domain/notification.go | 33 + internal/domain/url_template.go | 4 + internal/notification/channels/error.go | 25 + .../notification/channels/twilio/channel.go | 18 +- internal/notification/handlers/commands.go | 6 + .../notification/handlers/config_email.go | 3 + internal/notification/handlers/config_sms.go | 3 + internal/notification/handlers/ctx.go | 4 + .../handlers/mock/commands.mock.go | 58 + .../handlers/notification_worker.go | 515 ++++ .../handlers/notification_worker_test.go | 963 ++++++ .../notification/handlers/user_notifier.go | 806 +++-- .../handlers/user_notifier_test.go | 2709 +++++++++-------- internal/notification/projections.go | 12 +- internal/notification/types/domain_claimed.go | 20 - .../types/email_verification_code.go | 28 - .../types/email_verification_code_test.go | 92 - internal/notification/types/init_code.go | 17 - internal/notification/types/invite_code.go | 31 - internal/notification/types/notification.go | 22 +- internal/notification/types/otp.go | 29 - .../notification/types/password_change.go | 16 - internal/notification/types/password_code.go | 27 - .../types/passwordless_registration_link.go | 25 - .../passwordless_registration_link_test.go | 90 - .../types/phone_verification_code.go | 15 - internal/notification/types/types_test.go | 23 - internal/notification/types/user_email.go | 3 + internal/repository/notification/aggregate.go | 25 + .../repository/notification/eventstore.go | 12 + .../repository/notification/notification.go | 244 ++ 45 files changed, 4005 insertions(+), 2158 deletions(-) create mode 100644 internal/command/notification.go create mode 100644 internal/notification/channels/error.go create mode 100644 internal/notification/handlers/notification_worker.go create mode 100644 internal/notification/handlers/notification_worker_test.go delete mode 100644 internal/notification/types/domain_claimed.go delete mode 100644 internal/notification/types/email_verification_code.go delete mode 100644 internal/notification/types/email_verification_code_test.go delete mode 100644 internal/notification/types/init_code.go delete mode 100644 internal/notification/types/invite_code.go delete mode 100644 internal/notification/types/otp.go delete mode 100644 internal/notification/types/password_change.go delete mode 100644 internal/notification/types/password_code.go delete mode 100644 internal/notification/types/passwordless_registration_link.go delete mode 100644 internal/notification/types/passwordless_registration_link_test.go delete mode 100644 internal/notification/types/phone_verification_code.go delete mode 100644 internal/notification/types/types_test.go create mode 100644 internal/repository/notification/aggregate.go create mode 100644 internal/repository/notification/eventstore.go create mode 100644 internal/repository/notification/notification.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac7d909589..181ec838fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: go_version: "1.22" node_version: "18" buf_version: "latest" - go_lint_version: "v1.55.2" + go_lint_version: "v1.62.2" core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_path: ${{ needs.core.outputs.cache_path }} diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index e15d491a8b..16c321251a 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -448,6 +448,40 @@ Projections: # Telemetry data synchronization is not time critical. Setting RequeueEvery to 55 minutes doesn't annoy the database too much. RequeueEvery: 3300s # ZITADEL_PROJECTIONS_CUSTOMIZATIONS_TELEMETRY_REQUEUEEVERY +Notifications: + # The amount of workers processing the notification request events. + # If set to 0, no notification request events will be handled. This can be useful when running in + # multi binary / pod setup and allowing only certain executables to process the events. + Workers: 1 # ZITADEL_NOTIFIACATIONS_WORKERS + # The amount of events a single worker will process in a run. + BulkLimit: 10 # ZITADEL_NOTIFIACATIONS_BULKLIMIT + # Time interval between scheduled notifications for request events + RequeueEvery: 2s # ZITADEL_NOTIFIACATIONS_REQUEUEEVERY + # The amount of workers processing the notification retry events. + # If set to 0, no notification retry events will be handled. This can be useful when running in + # multi binary / pod setup and allowing only certain executables to process the events. + RetryWorkers: 1 # ZITADEL_NOTIFIACATIONS_RETRYWORKERS + # Time interval between scheduled notifications for retry events + RetryRequeueEvery: 2s # ZITADEL_NOTIFIACATIONS_RETRYREQUEUEEVERY + # Only instances are projected, for which at least a projection-relevant event exists within the timeframe + # from HandleActiveInstances duration in the past until the projection's current time + # If set to 0 (default), every instance is always considered active + HandleActiveInstances: 0s # ZITADEL_NOTIFIACATIONS_HANDLEACTIVEINSTANCES + # The maximum duration a transaction remains open + # before it spots left folding additional events + # and updates the table. + TransactionDuration: 1m # ZITADEL_NOTIFIACATIONS_TRANSACTIONDURATION + # Automatically cancel the notification after the amount of failed attempts + MaxAttempts: 3 # ZITADEL_NOTIFIACATIONS_MAXATTEMPTS + # Automatically cancel the notification if it cannot be handled within a specific time + MaxTtl: 5m # ZITADEL_NOTIFIACATIONS_MAXTTL + # Failed attempts are retried after a confogired delay (with exponential backoff). + # Set a minimum and maximum delay and a factor for the backoff + MinRetryDelay: 1s # ZITADEL_NOTIFIACATIONS_MINRETRYDELAY + MaxRetryDelay: 20s # ZITADEL_NOTIFIACATIONS_MAXRETRYDELAY + # Any factor below 1 will be set to 1 + RetryDelayFactor: 1.5 # ZITADEL_NOTIFIACATIONS_RETRYDELAYFACTOR + Auth: # See Projections.BulkLimit SearchLimit: 1000 # ZITADEL_AUTH_SEARCHLIMIT diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index cffc4921ca..f849d01217 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -69,6 +69,7 @@ func projectionsCmd() *cobra.Command { type ProjectionsConfig struct { Destination database.Config Projections projection.Config + Notifications handlers.WorkerConfig EncryptionKeys *encryption.EncryptionKeyConfig SystemAPIUsers map[string]*internal_authz.SystemAPIUser Eventstore *eventstore.Config @@ -205,6 +206,7 @@ func projections( config.Projections.Customizations["notificationsquotas"], config.Projections.Customizations["backchannel"], config.Projections.Customizations["telemetry"], + config.Notifications, *config.Telemetry, config.ExternalDomain, config.ExternalPort, @@ -219,6 +221,7 @@ func projections( keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, + client, ) config.Auth.Spooler.Client = client diff --git a/cmd/setup/config.go b/cmd/setup/config.go index b0a143b698..bd5444f2de 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -42,6 +42,7 @@ type Config struct { DefaultInstance command.InstanceSetup Machine *id.Config Projections projection.Config + Notifications handlers.WorkerConfig Eventstore *eventstore.Config InitProjections InitProjections diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index b8ea708cbf..1ad3037009 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -437,6 +437,7 @@ func initProjections( config.Projections.Customizations["notificationsquotas"], config.Projections.Customizations["backchannel"], config.Projections.Customizations["telemetry"], + config.Notifications, *config.Telemetry, config.ExternalDomain, config.ExternalPort, @@ -451,6 +452,7 @@ func initProjections( keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, + queryDBClient, ) for _, p := range notify_handler.Projections() { err := migration.Migrate(ctx, eventstoreClient, p) diff --git a/cmd/start/config.go b/cmd/start/config.go index 26c4b84b50..6182342592 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -54,6 +54,7 @@ type Config struct { Metrics metrics.Config Profiler profiler.Config Projections projection.Config + Notifications handlers.WorkerConfig Auth auth_es.Config Admin admin_es.Config UserAgentCookie *middleware.UserAgentCookieConfig diff --git a/cmd/start/start.go b/cmd/start/start.go index e816b5bb52..c9147fe653 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -277,6 +277,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server config.Projections.Customizations["notificationsquotas"], config.Projections.Customizations["backchannel"], config.Projections.Customizations["telemetry"], + config.Notifications, *config.Telemetry, config.ExternalDomain, config.ExternalPort, @@ -291,6 +292,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server keys.SMS, keys.OIDC, config.OIDC.DefaultBackChannelLogoutLifetime, + queryDBClient, ) notification.Start(ctx) diff --git a/internal/api/ui/login/init_password_handler.go b/internal/api/ui/login/init_password_handler.go index 4b9c173a2f..f7faab778e 100644 --- a/internal/api/ui/login/init_password_handler.go +++ b/internal/api/ui/login/init_password_handler.go @@ -1,8 +1,8 @@ package login import ( + "fmt" "net/http" - "net/url" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/domain" @@ -38,13 +38,13 @@ type initPasswordData struct { HasSymbol string } -func InitPasswordLink(origin, userID, code, orgID, authRequestID string) string { - v := url.Values{} - v.Set(queryInitPWUserID, userID) - v.Set(queryInitPWCode, code) - v.Set(queryOrgID, orgID) - v.Set(QueryAuthRequestID, authRequestID) - return externalLink(origin) + EndpointInitPassword + "?" + v.Encode() +func InitPasswordLinkTemplate(origin, userID, orgID, authRequestID string) string { + return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%s&%s=%s", + externalLink(origin), EndpointInitPassword, + queryInitPWUserID, userID, + queryInitPWCode, "{{.Code}}", + queryOrgID, orgID, + QueryAuthRequestID, authRequestID) } func (l *Login) handleInitPassword(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/init_user_handler.go b/internal/api/ui/login/init_user_handler.go index d60c4c3cbd..ad00aa0258 100644 --- a/internal/api/ui/login/init_user_handler.go +++ b/internal/api/ui/login/init_user_handler.go @@ -1,8 +1,8 @@ package login import ( + "fmt" "net/http" - "net/url" "strconv" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" @@ -44,15 +44,15 @@ type initUserData struct { HasSymbol string } -func InitUserLink(origin, userID, loginName, code, orgID string, passwordSet bool, authRequestID string) string { - v := url.Values{} - v.Set(queryInitUserUserID, userID) - v.Set(queryInitUserLoginName, loginName) - v.Set(queryInitUserCode, code) - v.Set(queryOrgID, orgID) - v.Set(queryInitUserPassword, strconv.FormatBool(passwordSet)) - v.Set(QueryAuthRequestID, authRequestID) - return externalLink(origin) + EndpointInitUser + "?" + v.Encode() +func InitUserLinkTemplate(origin, userID, orgID, authRequestID string) string { + return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%s&%s=%s&%s=%s&%s=%s", + externalLink(origin), EndpointInitUser, + queryInitUserUserID, userID, + queryInitUserLoginName, "{{.LoginName}}", + queryInitUserCode, "{{.Code}}", + queryOrgID, orgID, + queryInitUserPassword, "{{.PasswordSet}}", + QueryAuthRequestID, authRequestID) } func (l *Login) handleInitUser(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/invite_user_handler.go b/internal/api/ui/login/invite_user_handler.go index 3141af2d78..9f9ffb5ad3 100644 --- a/internal/api/ui/login/invite_user_handler.go +++ b/internal/api/ui/login/invite_user_handler.go @@ -1,8 +1,8 @@ package login import ( + "fmt" "net/http" - "net/url" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/domain" @@ -40,14 +40,14 @@ type inviteUserData struct { HasSymbol string } -func InviteUserLink(origin, userID, loginName, code, orgID string, authRequestID string) string { - v := url.Values{} - v.Set(queryInviteUserUserID, userID) - v.Set(queryInviteUserLoginName, loginName) - v.Set(queryInviteUserCode, code) - v.Set(queryOrgID, orgID) - v.Set(QueryAuthRequestID, authRequestID) - return externalLink(origin) + EndpointInviteUser + "?" + v.Encode() +func InviteUserLinkTemplate(origin, userID, orgID string, authRequestID string) string { + return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%s&%s=%s&%s=%s", + externalLink(origin), EndpointInviteUser, + queryInviteUserUserID, userID, + queryInviteUserLoginName, "{{.LoginName}}", + queryInviteUserCode, "{{.Code}}", + queryOrgID, orgID, + QueryAuthRequestID, authRequestID) } func (l *Login) handleInviteUser(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/mail_verify_handler.go b/internal/api/ui/login/mail_verify_handler.go index be13663529..5be22c6741 100644 --- a/internal/api/ui/login/mail_verify_handler.go +++ b/internal/api/ui/login/mail_verify_handler.go @@ -2,8 +2,8 @@ package login import ( "context" + "fmt" "net/http" - "net/url" "slices" "github.com/zitadel/logging" @@ -43,13 +43,13 @@ type mailVerificationData struct { HasSymbol string } -func MailVerificationLink(origin, userID, code, orgID, authRequestID string) string { - v := url.Values{} - v.Set(queryUserID, userID) - v.Set(queryCode, code) - v.Set(queryOrgID, orgID) - v.Set(QueryAuthRequestID, authRequestID) - return externalLink(origin) + EndpointMailVerification + "?" + v.Encode() +func MailVerificationLinkTemplate(origin, userID, orgID, authRequestID string) string { + return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%s&%s=%s", + externalLink(origin), EndpointMailVerification, + queryUserID, userID, + queryCode, "{{.Code}}", + queryOrgID, orgID, + QueryAuthRequestID, authRequestID) } func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/mfa_verify_otp_handler.go b/internal/api/ui/login/mfa_verify_otp_handler.go index b39605e667..09352f9443 100644 --- a/internal/api/ui/login/mfa_verify_otp_handler.go +++ b/internal/api/ui/login/mfa_verify_otp_handler.go @@ -27,8 +27,8 @@ type mfaOTPFormData struct { Provider domain.MFAType `schema:"provider"` } -func OTPLink(origin, authRequestID, code string, provider domain.MFAType) string { - return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%d", externalLink(origin), EndpointMFAOTPVerify, QueryAuthRequestID, authRequestID, queryCode, code, querySelectedProvider, provider) +func OTPLinkTemplate(origin, authRequestID string, provider domain.MFAType) string { + return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%d", externalLink(origin), EndpointMFAOTPVerify, QueryAuthRequestID, authRequestID, queryCode, "{{.Code}}", querySelectedProvider, provider) } // renderOTPVerification renders the OTP verification for SMS and Email based on the passed MFAType. diff --git a/internal/command/notification.go b/internal/command/notification.go new file mode 100644 index 0000000000..b0524afa89 --- /dev/null +++ b/internal/command/notification.go @@ -0,0 +1,162 @@ +package command + +import ( + "context" + "database/sql" + "time" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/notification" +) + +type NotificationRequest struct { + UserID string + UserResourceOwner string + TriggerOrigin string + URLTemplate string + Code *crypto.CryptoValue + CodeExpiry time.Duration + EventType eventstore.EventType + NotificationType domain.NotificationType + MessageType string + UnverifiedNotificationChannel bool + Args *domain.NotificationArguments + AggregateID string + AggregateResourceOwner string + IsOTP bool + RequiresPreviousDomain bool +} + +type NotificationRetryRequest struct { + NotificationRequest + BackOff time.Duration + NotifyUser *query.NotifyUser +} + +func NewNotificationRequest( + userID, resourceOwner, triggerOrigin string, + eventType eventstore.EventType, + notificationType domain.NotificationType, + messageType string, +) *NotificationRequest { + return &NotificationRequest{ + UserID: userID, + UserResourceOwner: resourceOwner, + TriggerOrigin: triggerOrigin, + EventType: eventType, + NotificationType: notificationType, + MessageType: messageType, + } +} + +func (r *NotificationRequest) WithCode(code *crypto.CryptoValue, expiry time.Duration) *NotificationRequest { + r.Code = code + r.CodeExpiry = expiry + return r +} + +func (r *NotificationRequest) WithURLTemplate(urlTemplate string) *NotificationRequest { + r.URLTemplate = urlTemplate + return r +} + +func (r *NotificationRequest) WithUnverifiedChannel() *NotificationRequest { + r.UnverifiedNotificationChannel = true + return r +} + +func (r *NotificationRequest) WithArgs(args *domain.NotificationArguments) *NotificationRequest { + r.Args = args + return r +} + +func (r *NotificationRequest) WithAggregate(id, resourceOwner string) *NotificationRequest { + r.AggregateID = id + r.AggregateResourceOwner = resourceOwner + return r +} + +func (r *NotificationRequest) WithOTP() *NotificationRequest { + r.IsOTP = true + return r +} + +func (r *NotificationRequest) WithPreviousDomain() *NotificationRequest { + r.RequiresPreviousDomain = true + return r +} + +// RequestNotification writes a new notification.RequestEvent with the notification.Aggregate to the eventstore +func (c *Commands) RequestNotification( + ctx context.Context, + resourceOwner string, + request *NotificationRequest, +) error { + id, err := c.idGenerator.Next() + if err != nil { + return err + } + _, err = c.eventstore.Push(ctx, notification.NewRequestedEvent(ctx, ¬ification.NewAggregate(id, resourceOwner).Aggregate, + request.UserID, + request.UserResourceOwner, + request.AggregateID, + request.AggregateResourceOwner, + request.TriggerOrigin, + request.URLTemplate, + request.Code, + request.CodeExpiry, + request.EventType, + request.NotificationType, + request.MessageType, + request.UnverifiedNotificationChannel, + request.IsOTP, + request.RequiresPreviousDomain, + request.Args)) + return err +} + +// NotificationCanceled writes a new notification.CanceledEvent with the notification.Aggregate to the eventstore +func (c *Commands) NotificationCanceled(ctx context.Context, tx *sql.Tx, id, resourceOwner string, requestError error) error { + var errorMessage string + if requestError != nil { + errorMessage = requestError.Error() + } + _, err := c.eventstore.PushWithClient(ctx, tx, notification.NewCanceledEvent(ctx, ¬ification.NewAggregate(id, resourceOwner).Aggregate, errorMessage)) + return err +} + +// NotificationSent writes a new notification.SentEvent with the notification.Aggregate to the eventstore +func (c *Commands) NotificationSent(ctx context.Context, tx *sql.Tx, id, resourceOwner string) error { + _, err := c.eventstore.PushWithClient(ctx, tx, notification.NewSentEvent(ctx, ¬ification.NewAggregate(id, resourceOwner).Aggregate)) + return err +} + +// NotificationRetryRequested writes a new notification.RetryRequestEvent with the notification.Aggregate to the eventstore +func (c *Commands) NotificationRetryRequested(ctx context.Context, tx *sql.Tx, id, resourceOwner string, request *NotificationRetryRequest, requestError error) error { + var errorMessage string + if requestError != nil { + errorMessage = requestError.Error() + } + _, err := c.eventstore.PushWithClient(ctx, tx, notification.NewRetryRequestedEvent(ctx, ¬ification.NewAggregate(id, resourceOwner).Aggregate, + request.UserID, + request.UserResourceOwner, + request.AggregateID, + request.AggregateResourceOwner, + request.TriggerOrigin, + request.URLTemplate, + request.Code, + request.CodeExpiry, + request.EventType, + request.NotificationType, + request.MessageType, + request.UnverifiedNotificationChannel, + request.IsOTP, + request.Args, + request.NotifyUser, + request.BackOff, + errorMessage)) + return err +} diff --git a/internal/domain/human_web_auth_n.go b/internal/domain/human_web_auth_n.go index 16590d43ca..62c3424914 100644 --- a/internal/domain/human_web_auth_n.go +++ b/internal/domain/human_web_auth_n.go @@ -96,3 +96,7 @@ func (p *PasswordlessInitCode) Link(baseURL string) string { func PasswordlessInitCodeLink(baseURL, userID, resourceOwner, codeID, code string) string { return fmt.Sprintf("%s?userID=%s&orgID=%s&codeID=%s&code=%s", baseURL, userID, resourceOwner, codeID, code) } + +func PasswordlessInitCodeLinkTemplate(baseURL, userID, resourceOwner, codeID string) string { + return PasswordlessInitCodeLink(baseURL, userID, resourceOwner, codeID, "{{.Code}}") +} diff --git a/internal/domain/notification.go b/internal/domain/notification.go index 756c400c66..81bf6413dd 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -1,5 +1,9 @@ package domain +import ( + "time" +) + type NotificationType int32 const ( @@ -31,3 +35,32 @@ const ( notificationProviderTypeCount ) + +type NotificationArguments struct { + Origin string `json:"origin,omitempty"` + Domain string `json:"domain,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + TempUsername string `json:"tempUsername,omitempty"` + ApplicationName string `json:"applicationName,omitempty"` + CodeID string `json:"codeID,omitempty"` + SessionID string `json:"sessionID,omitempty"` + AuthRequestID string `json:"authRequestID,omitempty"` +} + +// ToMap creates a type safe map of the notification arguments. +// Since these arguments are used in text template, all keys must be PascalCase and types must remain the same (e.g. Duration). +func (n *NotificationArguments) ToMap() map[string]interface{} { + m := make(map[string]interface{}) + if n == nil { + return m + } + m["Origin"] = n.Origin + m["Domain"] = n.Domain + m["Expiry"] = n.Expiry + m["TempUsername"] = n.TempUsername + m["ApplicationName"] = n.ApplicationName + m["CodeID"] = n.CodeID + m["SessionID"] = n.SessionID + m["AuthRequestID"] = n.AuthRequestID + return m +} diff --git a/internal/domain/url_template.go b/internal/domain/url_template.go index ed39a8257e..063d701d0a 100644 --- a/internal/domain/url_template.go +++ b/internal/domain/url_template.go @@ -7,6 +7,10 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +func RenderURLTemplate(w io.Writer, tmpl string, data any) error { + return renderURLTemplate(w, tmpl, data) +} + func renderURLTemplate(w io.Writer, tmpl string, data any) error { parsed, err := template.New("").Parse(tmpl) if err != nil { diff --git a/internal/notification/channels/error.go b/internal/notification/channels/error.go new file mode 100644 index 0000000000..e3c3fa3c49 --- /dev/null +++ b/internal/notification/channels/error.go @@ -0,0 +1,25 @@ +package channels + +import "errors" + +type CancelError struct { + Err error +} + +func (e *CancelError) Error() string { + return e.Err.Error() +} + +func NewCancelError(err error) error { + return &CancelError{ + Err: err, + } +} + +func (e *CancelError) Is(target error) bool { + return errors.As(target, &e) +} + +func (e *CancelError) Unwrap() error { + return e.Err +} diff --git a/internal/notification/channels/twilio/channel.go b/internal/notification/channels/twilio/channel.go index e3e2767a0e..8b7f0e24f2 100644 --- a/internal/notification/channels/twilio/channel.go +++ b/internal/notification/channels/twilio/channel.go @@ -1,7 +1,10 @@ package twilio import ( - newTwilio "github.com/twilio/twilio-go" + "errors" + + "github.com/twilio/twilio-go" + twilioClient "github.com/twilio/twilio-go/client" openapi "github.com/twilio/twilio-go/rest/api/v2010" verify "github.com/twilio/twilio-go/rest/verify/v2" "github.com/zitadel/logging" @@ -12,7 +15,7 @@ import ( ) func InitChannel(config Config) channels.NotificationChannel { - client := newTwilio.NewRestClientWithParams(newTwilio.ClientParams{Username: config.SID, Password: config.Token}) + client := twilio.NewRestClientWithParams(twilio.ClientParams{Username: config.SID, Password: config.Token}) logging.Debug("successfully initialized twilio sms channel") return channels.HandleMessageFunc(func(message channels.Message) error { @@ -26,6 +29,17 @@ func InitChannel(config Config) channels.NotificationChannel { params.SetChannel("sms") resp, err := client.VerifyV2.CreateVerification(config.VerifyServiceSID, params) + + var twilioErr *twilioClient.TwilioRestError + if errors.As(err, &twilioErr) && twilioErr.Code == 60203 { + // If there were too many attempts to send a verification code (more than 5 times) + // without a verification check, even retries with backoff might not solve the problem. + // Instead, let the user initiate the verification again (e.g. using "resend code") + // https://www.twilio.com/docs/api/errors/60203 + logging.WithFields("error", twilioErr.Message, "code", twilioErr.Code).Warn("twilio create verification error") + return channels.NewCancelError(twilioErr) + } + if err != nil { return zerrors.ThrowInternal(err, "TWILI-0s9f2", "could not send verification") } diff --git a/internal/notification/handlers/commands.go b/internal/notification/handlers/commands.go index 07969a6bba..90b66bdf48 100644 --- a/internal/notification/handlers/commands.go +++ b/internal/notification/handlers/commands.go @@ -2,13 +2,19 @@ package handlers import ( "context" + "database/sql" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/repository/milestone" "github.com/zitadel/zitadel/internal/repository/quota" ) type Commands interface { + RequestNotification(ctx context.Context, instanceID string, request *command.NotificationRequest) error + NotificationCanceled(ctx context.Context, tx *sql.Tx, id, resourceOwner string, err error) error + NotificationRetryRequested(ctx context.Context, tx *sql.Tx, id, resourceOwner string, request *command.NotificationRetryRequest, err error) error + NotificationSent(ctx context.Context, tx *sql.Tx, id, instanceID string) error HumanInitCodeSent(ctx context.Context, orgID, userID string) error HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) error PasswordCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error diff --git a/internal/notification/handlers/config_email.go b/internal/notification/handlers/config_email.go index b78540a423..3e6eaa27a1 100644 --- a/internal/notification/handlers/config_email.go +++ b/internal/notification/handlers/config_email.go @@ -23,6 +23,9 @@ func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email. Description: config.Description, } if config.SMTPConfig != nil { + if config.SMTPConfig.Password == nil { + return nil, zerrors.ThrowNotFound(err, "QUERY-Wrs3gw", "Errors.SMTPConfig.NotFound") + } password, err := crypto.DecryptString(config.SMTPConfig.Password, n.SMTPPasswordCrypto) if err != nil { return nil, err diff --git a/internal/notification/handlers/config_sms.go b/internal/notification/handlers/config_sms.go index 1962824c9a..fd733b3731 100644 --- a/internal/notification/handlers/config_sms.go +++ b/internal/notification/handlers/config_sms.go @@ -24,6 +24,9 @@ func (n *NotificationQueries) GetActiveSMSConfig(ctx context.Context) (*sms.Conf Description: config.Description, } if config.TwilioConfig != nil { + if config.TwilioConfig.Token == nil { + return nil, zerrors.ThrowNotFound(err, "QUERY-SFefsd", "Errors.SMS.Twilio.NotFound") + } token, err := crypto.DecryptString(config.TwilioConfig.Token, n.SMSTokenCrypto) if err != nil { return nil, err diff --git a/internal/notification/handlers/ctx.go b/internal/notification/handlers/ctx.go index b8fc45da68..8f499814aa 100644 --- a/internal/notification/handlers/ctx.go +++ b/internal/notification/handlers/ctx.go @@ -14,6 +14,10 @@ func HandlerContext(event *eventstore.Aggregate) context.Context { return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: event.ResourceOwner}) } +func ContextWithNotifier(ctx context.Context, aggregate *eventstore.Aggregate) context.Context { + return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: aggregate.ResourceOwner}) +} + func (n *NotificationQueries) HandlerContext(event *eventstore.Aggregate) (context.Context, error) { ctx := context.Background() instance, err := n.InstanceByID(ctx, event.InstanceID) diff --git a/internal/notification/handlers/mock/commands.mock.go b/internal/notification/handlers/mock/commands.mock.go index ec327de8e8..ee6eb3c6b1 100644 --- a/internal/notification/handlers/mock/commands.mock.go +++ b/internal/notification/handlers/mock/commands.mock.go @@ -11,8 +11,10 @@ package mock import ( context "context" + sql "database/sql" reflect "reflect" + command "github.com/zitadel/zitadel/internal/command" senders "github.com/zitadel/zitadel/internal/notification/senders" milestone "github.com/zitadel/zitadel/internal/repository/milestone" quota "github.com/zitadel/zitadel/internal/repository/quota" @@ -155,6 +157,48 @@ func (mr *MockCommandsMockRecorder) MilestonePushed(ctx, instanceID, msType, end return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), ctx, instanceID, msType, endpoints) } +// NotificationCanceled mocks base method. +func (m *MockCommands) NotificationCanceled(ctx context.Context, tx *sql.Tx, id, resourceOwner string, err error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NotificationCanceled", ctx, tx, id, resourceOwner, err) + ret0, _ := ret[0].(error) + return ret0 +} + +// NotificationCanceled indicates an expected call of NotificationCanceled. +func (mr *MockCommandsMockRecorder) NotificationCanceled(ctx, tx, id, resourceOwner, err any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationCanceled", reflect.TypeOf((*MockCommands)(nil).NotificationCanceled), ctx, tx, id, resourceOwner, err) +} + +// NotificationRetryRequested mocks base method. +func (m *MockCommands) NotificationRetryRequested(ctx context.Context, tx *sql.Tx, id, resourceOwner string, request *command.NotificationRetryRequest, err error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NotificationRetryRequested", ctx, tx, id, resourceOwner, request, err) + ret0, _ := ret[0].(error) + return ret0 +} + +// NotificationRetryRequested indicates an expected call of NotificationRetryRequested. +func (mr *MockCommandsMockRecorder) NotificationRetryRequested(ctx, tx, id, resourceOwner, request, err any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationRetryRequested", reflect.TypeOf((*MockCommands)(nil).NotificationRetryRequested), ctx, tx, id, resourceOwner, request, err) +} + +// NotificationSent mocks base method. +func (m *MockCommands) NotificationSent(ctx context.Context, tx *sql.Tx, id, instanceID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NotificationSent", ctx, tx, id, instanceID) + ret0, _ := ret[0].(error) + return ret0 +} + +// NotificationSent indicates an expected call of NotificationSent. +func (mr *MockCommandsMockRecorder) NotificationSent(ctx, tx, id, instanceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationSent", reflect.TypeOf((*MockCommands)(nil).NotificationSent), ctx, tx, id, instanceID) +} + // OTPEmailSent mocks base method. func (m *MockCommands) OTPEmailSent(ctx context.Context, sessionID, resourceOwner string) error { m.ctrl.T.Helper() @@ -211,6 +255,20 @@ func (mr *MockCommandsMockRecorder) PasswordCodeSent(ctx, orgID, userID, generat return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), ctx, orgID, userID, generatorInfo) } +// RequestNotification mocks base method. +func (m *MockCommands) RequestNotification(ctx context.Context, instanceID string, request *command.NotificationRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RequestNotification", ctx, instanceID, request) + ret0, _ := ret[0].(error) + return ret0 +} + +// RequestNotification indicates an expected call of RequestNotification. +func (mr *MockCommandsMockRecorder) RequestNotification(ctx, instanceID, request any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestNotification", reflect.TypeOf((*MockCommands)(nil).RequestNotification), ctx, instanceID, request) +} + // UsageNotificationSent mocks base method. func (m *MockCommands) UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error { m.ctrl.T.Helper() diff --git a/internal/notification/handlers/notification_worker.go b/internal/notification/handlers/notification_worker.go new file mode 100644 index 0000000000..96ecd755dd --- /dev/null +++ b/internal/notification/handlers/notification_worker.go @@ -0,0 +1,515 @@ +package handlers + +import ( + "context" + "database/sql" + "errors" + "math/rand/v2" + "slices" + "strings" + "time" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/notification/channels" + "github.com/zitadel/zitadel/internal/notification/senders" + "github.com/zitadel/zitadel/internal/notification/types" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/notification" +) + +const ( + Domain = "Domain" + Code = "Code" + OTP = "OTP" +) + +type NotificationWorker struct { + commands Commands + queries *NotificationQueries + es *eventstore.Eventstore + client *database.DB + channels types.ChannelChains + config WorkerConfig + now nowFunc + backOff func(current time.Duration) time.Duration +} + +type WorkerConfig struct { + Workers uint8 + BulkLimit uint16 + RequeueEvery time.Duration + RetryWorkers uint8 + RetryRequeueEvery time.Duration + HandleActiveInstances time.Duration + TransactionDuration time.Duration + MaxAttempts uint8 + MaxTtl time.Duration + MinRetryDelay time.Duration + MaxRetryDelay time.Duration + RetryDelayFactor float32 +} + +// nowFunc makes [time.Now] mockable +type nowFunc func() time.Time + +type Sent func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error + +var sentHandlers map[eventstore.EventType]Sent + +func RegisterSentHandler(eventType eventstore.EventType, sent Sent) { + if sentHandlers == nil { + sentHandlers = make(map[eventstore.EventType]Sent) + } + sentHandlers[eventType] = sent +} + +func NewNotificationWorker( + config WorkerConfig, + commands Commands, + queries *NotificationQueries, + es *eventstore.Eventstore, + client *database.DB, + channels types.ChannelChains, +) *NotificationWorker { + // make sure the delay does not get less + if config.RetryDelayFactor < 1 { + config.RetryDelayFactor = 1 + } + w := &NotificationWorker{ + config: config, + commands: commands, + queries: queries, + es: es, + client: client, + channels: channels, + now: time.Now, + } + w.backOff = w.exponentialBackOff + return w +} + +func (w *NotificationWorker) Start(ctx context.Context) { + for i := 0; i < int(w.config.Workers); i++ { + go w.schedule(ctx, i, false) + } + for i := 0; i < int(w.config.RetryWorkers); i++ { + go w.schedule(ctx, i, true) + } +} + +func (w *NotificationWorker) reduceNotificationRequested(ctx context.Context, tx *sql.Tx, event *notification.RequestedEvent) (err error) { + ctx = ContextWithNotifier(ctx, event.Aggregate()) + + // if the notification is too old, we can directly cancel + if event.CreatedAt().Add(w.config.MaxTtl).Before(w.now()) { + return w.commands.NotificationCanceled(ctx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, nil) + } + + // Get the notify user first, so if anything fails afterward we have the current state of the user + // and can pass that to the retry request. + notifyUser, err := w.queries.GetNotifyUserByID(ctx, true, event.UserID) + if err != nil { + return err + } + + // The domain claimed event requires the domain as argument, but lacks the user when creating the request event. + // Since we set it into the request arguments, it will be passed into a potential retry event. + if event.RequiresPreviousDomain && event.Request.Args != nil && event.Request.Args.Domain == "" { + index := strings.LastIndex(notifyUser.LastEmail, "@") + event.Request.Args.Domain = notifyUser.LastEmail[index+1:] + } + + err = w.sendNotification(ctx, tx, event.Request, notifyUser, event) + if err == nil { + return nil + } + // if retries are disabled or if the error explicitly specifies, we cancel the notification + if w.config.MaxAttempts <= 1 || errors.Is(err, &channels.CancelError{}) { + return w.commands.NotificationCanceled(ctx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, err) + } + // otherwise we retry after a backoff delay + return w.commands.NotificationRetryRequested( + ctx, + tx, + event.Aggregate().ID, + event.Aggregate().ResourceOwner, + notificationEventToRequest(event.Request, notifyUser, w.backOff(0)), + err, + ) +} + +func (w *NotificationWorker) reduceNotificationRetry(ctx context.Context, tx *sql.Tx, event *notification.RetryRequestedEvent) (err error) { + ctx = ContextWithNotifier(ctx, event.Aggregate()) + + // if the notification is too old, we can directly cancel + if event.CreatedAt().Add(w.config.MaxTtl).Before(w.now()) { + return w.commands.NotificationCanceled(ctx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, err) + } + + if event.CreatedAt().Add(event.BackOff).After(w.now()) { + return nil + } + err = w.sendNotification(ctx, tx, event.Request, event.NotifyUser, event) + if err == nil { + return nil + } + // if the max attempts are reached or if the error explicitly specifies, we cancel the notification + if event.Sequence() >= uint64(w.config.MaxAttempts) || errors.Is(err, &channels.CancelError{}) { + return w.commands.NotificationCanceled(ctx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, err) + } + // otherwise we retry after a backoff delay + return w.commands.NotificationRetryRequested(ctx, tx, event.Aggregate().ID, event.Aggregate().ResourceOwner, notificationEventToRequest( + event.Request, + event.NotifyUser, + w.backOff(event.BackOff), + ), err) +} + +func (w *NotificationWorker) sendNotification(ctx context.Context, tx *sql.Tx, request notification.Request, notifyUser *query.NotifyUser, e eventstore.Event) error { + ctx, err := enrichCtx(ctx, request.TriggeredAtOrigin) + if err != nil { + err := w.commands.NotificationCanceled(ctx, tx, e.Aggregate().ID, e.Aggregate().ResourceOwner, err) + logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "notification", e.Aggregate().ID). + OnError(err).Error("could not cancel notification") + return nil + } + + // check early that a "sent" handler exists, otherwise we can cancel early + sentHandler, ok := sentHandlers[request.EventType] + if !ok { + err := w.commands.NotificationCanceled(ctx, tx, e.Aggregate().ID, e.Aggregate().ResourceOwner, err) + logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "notification", e.Aggregate().ID). + OnError(err).Errorf(`no "sent" handler registered for %s`, request.EventType) + return nil + } + + var code string + if request.Code != nil { + code, err = crypto.DecryptString(request.Code, w.queries.UserDataCrypto) + if err != nil { + return err + } + } + + colors, err := w.queries.ActiveLabelPolicyByOrg(ctx, request.UserResourceOwner, false) + if err != nil { + return err + } + + translator, err := w.queries.GetTranslatorWithOrgTexts(ctx, request.UserResourceOwner, request.MessageType) + if err != nil { + return err + } + + generatorInfo := new(senders.CodeGeneratorInfo) + var notify types.Notify + switch request.NotificationType { + case domain.NotificationTypeEmail: + template, err := w.queries.MailTemplateByOrg(ctx, notifyUser.ResourceOwner, false) + if err != nil { + return err + } + notify = types.SendEmail(ctx, w.channels, string(template.Template), translator, notifyUser, colors, e) + case domain.NotificationTypeSms: + notify = types.SendSMS(ctx, w.channels, translator, notifyUser, colors, e, generatorInfo) + } + + args := request.Args.ToMap() + args[Code] = code + // existing notifications use `OTP` as argument for the code + if request.IsOTP { + args[OTP] = code + } + + if err := notify(request.URLTemplate, args, request.MessageType, request.UnverifiedNotificationChannel); err != nil { + return err + } + err = w.commands.NotificationSent(ctx, tx, e.Aggregate().ID, e.Aggregate().ResourceOwner) + if err != nil { + // In case the notification event cannot be pushed, we most likely cannot create a retry or cancel event. + // Therefore, we'll only log the error and also do not need to try to push to the user / session. + logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "notification", e.Aggregate().ID). + OnError(err).Error("could not set sent notification event") + return nil + } + err = sentHandler(ctx, w.commands, request.NotificationAggregateID(), request.NotificationAggregateResourceOwner(), generatorInfo, args) + logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "notification", e.Aggregate().ID). + OnError(err).Error("could not set notification event on aggregate") + return nil +} + +func (w *NotificationWorker) exponentialBackOff(current time.Duration) time.Duration { + if current >= w.config.MaxRetryDelay { + return w.config.MaxRetryDelay + } + if current < w.config.MinRetryDelay { + current = w.config.MinRetryDelay + } + t := time.Duration(rand.Int64N(int64(w.config.RetryDelayFactor*float32(current.Nanoseconds()))-current.Nanoseconds()) + current.Nanoseconds()) + if t > w.config.MaxRetryDelay { + return w.config.MaxRetryDelay + } + return t +} + +func notificationEventToRequest(e notification.Request, notifyUser *query.NotifyUser, backoff time.Duration) *command.NotificationRetryRequest { + return &command.NotificationRetryRequest{ + NotificationRequest: command.NotificationRequest{ + UserID: e.UserID, + UserResourceOwner: e.UserResourceOwner, + TriggerOrigin: e.TriggeredAtOrigin, + URLTemplate: e.URLTemplate, + Code: e.Code, + CodeExpiry: e.CodeExpiry, + EventType: e.EventType, + NotificationType: e.NotificationType, + MessageType: e.MessageType, + UnverifiedNotificationChannel: e.UnverifiedNotificationChannel, + Args: e.Args, + AggregateID: e.AggregateID, + AggregateResourceOwner: e.AggregateResourceOwner, + IsOTP: e.IsOTP, + }, + BackOff: backoff, + NotifyUser: notifyUser, + } +} + +func (w *NotificationWorker) schedule(ctx context.Context, workerID int, retry bool) { + t := time.NewTimer(0) + + for { + select { + case <-ctx.Done(): + t.Stop() + w.log(workerID, retry).Info("scheduler stopped") + return + case <-t.C: + instances, err := w.queryInstances(ctx, retry) + w.log(workerID, retry).OnError(err).Error("unable to query instances") + + w.triggerInstances(call.WithTimestamp(ctx), instances, workerID, retry) + if retry { + t.Reset(w.config.RetryRequeueEvery) + continue + } + t.Reset(w.config.RequeueEvery) + } + } +} + +func (w *NotificationWorker) log(workerID int, retry bool) *logging.Entry { + return logging.WithFields("notification worker", workerID, "retries", retry) +} + +func (w *NotificationWorker) queryInstances(ctx context.Context, retry bool) ([]string, error) { + if w.config.HandleActiveInstances == 0 { + return w.existingInstances(ctx) + } + + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsInstanceIDs). + AwaitOpenTransactions(). + AllowTimeTravel(). + CreationDateAfter(w.now().Add(-1 * w.config.HandleActiveInstances)) + + maxAge := w.config.RequeueEvery + if retry { + maxAge = w.config.RetryRequeueEvery + } + return w.es.InstanceIDs(ctx, maxAge, false, query) +} + +func (w *NotificationWorker) existingInstances(ctx context.Context) ([]string, error) { + ai := existingInstances{} + if err := w.es.FilterToQueryReducer(ctx, &ai); err != nil { + return nil, err + } + + return ai, nil +} + +func (w *NotificationWorker) triggerInstances(ctx context.Context, instances []string, workerID int, retry bool) { + for _, instance := range instances { + instanceCtx := authz.WithInstanceID(ctx, instance) + + err := w.trigger(instanceCtx, workerID, retry) + w.log(workerID, retry).WithField("instance", instance).OnError(err).Info("trigger failed") + } +} + +func (w *NotificationWorker) trigger(ctx context.Context, workerID int, retry bool) (err error) { + txCtx := ctx + if w.config.TransactionDuration > 0 { + var cancel, cancelTx func() + txCtx, cancelTx = context.WithCancel(ctx) + defer cancelTx() + ctx, cancel = context.WithTimeout(ctx, w.config.TransactionDuration) + defer cancel() + } + tx, err := w.client.BeginTx(txCtx, nil) + if err != nil { + return err + } + defer func() { + err = database.CloseTransaction(tx, err) + }() + + events, err := w.searchEvents(ctx, tx, retry) + if err != nil { + return err + } + + // If there aren't any events or no unlocked event terminate early and start a new run. + if len(events) == 0 { + return nil + } + + w.log(workerID, retry). + WithField("instanceID", authz.GetInstance(ctx).InstanceID()). + WithField("events", len(events)). + Info("handling notification events") + + for _, event := range events { + var err error + switch e := event.(type) { + case *notification.RequestedEvent: + w.createSavepoint(ctx, tx, event, workerID, retry) + err = w.reduceNotificationRequested(ctx, tx, e) + case *notification.RetryRequestedEvent: + w.createSavepoint(ctx, tx, event, workerID, retry) + err = w.reduceNotificationRetry(ctx, tx, e) + } + if err != nil { + w.log(workerID, retry).OnError(err). + WithField("instanceID", authz.GetInstance(ctx).InstanceID()). + WithField("notificationID", event.Aggregate().ID). + WithField("sequence", event.Sequence()). + WithField("type", event.Type()). + Error("could not push notification event") + w.rollbackToSavepoint(ctx, tx, event, workerID, retry) + } + } + return nil +} + +func (w *NotificationWorker) latestRetries(events []eventstore.Event) { + for i := len(events) - 1; i > 0; i-- { + // since we delete during the iteration, we need to make sure we don't panic + if len(events) <= i { + continue + } + // delete all the previous retries of the same notification + events = slices.DeleteFunc(events, func(e eventstore.Event) bool { + return e.Aggregate().ID == events[i].Aggregate().ID && + e.Sequence() < events[i].Sequence() + }) + } +} + +func (w *NotificationWorker) createSavepoint(ctx context.Context, tx *sql.Tx, event eventstore.Event, workerID int, retry bool) { + _, err := tx.ExecContext(ctx, "SAVEPOINT notification_send") + w.log(workerID, retry).OnError(err). + WithField("instanceID", authz.GetInstance(ctx).InstanceID()). + WithField("notificationID", event.Aggregate().ID). + WithField("sequence", event.Sequence()). + WithField("type", event.Type()). + Error("could not create savepoint for notification event") +} + +func (w *NotificationWorker) rollbackToSavepoint(ctx context.Context, tx *sql.Tx, event eventstore.Event, workerID int, retry bool) { + _, err := tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT notification_send") + w.log(workerID, retry).OnError(err). + WithField("instanceID", authz.GetInstance(ctx).InstanceID()). + WithField("notificationID", event.Aggregate().ID). + WithField("sequence", event.Sequence()). + WithField("type", event.Type()). + Error("could not rollback to savepoint for notification event") +} + +func (w *NotificationWorker) searchEvents(ctx context.Context, tx *sql.Tx, retry bool) ([]eventstore.Event, error) { + if retry { + return w.searchRetryEvents(ctx, tx) + } + // query events and lock them for update (with skip locked) + searchQuery := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + LockRowsDuringTx(tx, eventstore.LockOptionSkipLocked). + // Messages older than the MaxTTL, we can be ignored. + // The first attempt of a retry might still be older than the TTL and needs to be filtered out later on. + CreationDateAfter(w.now().Add(-1*w.config.MaxTtl)). + Limit(uint64(w.config.BulkLimit)). + AddQuery(). + AggregateTypes(notification.AggregateType). + EventTypes(notification.RequestedType). + Builder(). + ExcludeAggregateIDs(). + EventTypes(notification.RetryRequestedType, notification.CanceledType, notification.SentType). + Builder() + //nolint:staticcheck + return w.es.Filter(ctx, searchQuery) +} + +func (w *NotificationWorker) searchRetryEvents(ctx context.Context, tx *sql.Tx) ([]eventstore.Event, error) { + // query events and lock them for update (with skip locked) + searchQuery := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + LockRowsDuringTx(tx, eventstore.LockOptionSkipLocked). + // Messages older than the MaxTTL, we can be ignored. + // The first attempt of a retry might still be older than the TTL and needs to be filtered out later on. + CreationDateAfter(w.now().Add(-1*w.config.MaxTtl)). + AddQuery(). + AggregateTypes(notification.AggregateType). + EventTypes(notification.RetryRequestedType). + Builder(). + ExcludeAggregateIDs(). + EventTypes(notification.CanceledType, notification.SentType). + Builder() + //nolint:staticcheck + events, err := w.es.Filter(ctx, searchQuery) + if err != nil { + return nil, err + } + w.latestRetries(events) + return events, nil +} + +type existingInstances []string + +// AppendEvents implements eventstore.QueryReducer. +func (ai *existingInstances) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + switch event.Type() { + case instance.InstanceAddedEventType: + *ai = append(*ai, event.Aggregate().InstanceID) + case instance.InstanceRemovedEventType: + *ai = slices.DeleteFunc(*ai, func(s string) bool { + return s == event.Aggregate().InstanceID + }) + } + } +} + +// Query implements eventstore.QueryReducer. +func (*existingInstances) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(instance.AggregateType). + EventTypes( + instance.InstanceAddedEventType, + instance.InstanceRemovedEventType, + ). + Builder() +} + +// Reduce implements eventstore.QueryReducer. +// reduce is not used as events are reduced during AppendEvents +func (*existingInstances) Reduce() error { + return nil +} diff --git a/internal/notification/handlers/notification_worker_test.go b/internal/notification/handlers/notification_worker_test.go new file mode 100644 index 0000000000..03de5201fc --- /dev/null +++ b/internal/notification/handlers/notification_worker_test.go @@ -0,0 +1,963 @@ +package handlers + +import ( + "context" + "database/sql" + "errors" + "fmt" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/ui/login" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock" + "github.com/zitadel/zitadel/internal/notification/channels/email" + channel_mock "github.com/zitadel/zitadel/internal/notification/channels/mock" + "github.com/zitadel/zitadel/internal/notification/channels/sms" + "github.com/zitadel/zitadel/internal/notification/channels/smtp" + "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/handlers/mock" + "github.com/zitadel/zitadel/internal/notification/messages" + "github.com/zitadel/zitadel/internal/notification/senders" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/notification" + "github.com/zitadel/zitadel/internal/repository/session" + "github.com/zitadel/zitadel/internal/repository/user" +) + +const ( + notificationID = "notificationID" +) + +func Test_userNotifier_reduceNotificationRequested(t *testing.T) { + testNow := time.Now + testBackOff := func(current time.Duration) time.Duration { + return time.Second + } + sendError := errors.New("send error") + tests := []struct { + name string + test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fieldsWorker, argsWorker, wantWorker) + }{ + { + name: "too old", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + codeAlg, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().NotificationCanceled(gomock.Any(), gomock.Any(), notificationID, instanceID, nil).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + }, + argsWorker{ + event: ¬ification.RequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().Add(-1 * time.Hour), + Typ: notification.RequestedType, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + }, + }, w + }, + }, + { + name: "send ok (email)", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = &messages.Email{ + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + } + codeAlg, code := cryptoValue(t, ctrl, "testcode") + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().NotificationSent(gomock.Any(), gomock.Any(), notificationID, instanceID).Return(nil) + commands.EXPECT().InviteCodeSent(gomock.Any(), orgID, userID).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + }, + argsWorker{ + event: ¬ification.RequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: notification.RequestedType, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + }, + }, w + }, + }, + { + name: "send ok (sms with external provider)", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + expiry := 0 * time.Hour + testCode := "" + expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. +@%[2]s #%[1]s`, testCode, eventOriginDomain, expiry) + w.messageSMS = &messages.SMS{ + SenderPhoneNumber: "senderNumber", + RecipientPhoneNumber: verifiedPhone, + Content: expectContent, + } + codeAlg, code := cryptoValue(t, ctrl, testCode) + expectTemplateWithNotifyUserQueriesSMS(queries) + commands.EXPECT().NotificationSent(gomock.Any(), gomock.Any(), notificationID, instanceID).Return(nil) + commands.EXPECT().OTPSMSSent(gomock.Any(), sessionID, instanceID, &senders.CodeGeneratorInfo{ + ID: smsProviderID, + VerificationID: verificationID, + }).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + }, + argsWorker{ + event: ¬ification.RequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: notification.RequestedType, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: sessionID, + AggregateResourceOwner: instanceID, + TriggeredAtOrigin: eventOrigin, + EventType: session.OTPSMSChallengedType, + MessageType: domain.VerifySMSOTPMessageType, + NotificationType: domain.NotificationTypeSms, + URLTemplate: "", + CodeExpiry: expiry, + Code: code, + UnverifiedNotificationChannel: false, + IsOTP: true, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + Origin: eventOrigin, + Domain: eventOriginDomain, + Expiry: expiry, + }, + }, + }, + }, w + }, + }, + { + name: "previous domain", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = &messages.Email{ + Recipients: []string{verifiedEmail}, + Subject: "Domain has been claimed", + Content: expectContent, + } + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().NotificationSent(gomock.Any(), gomock.Any(), notificationID, instanceID).Return(nil) + commands.EXPECT().UserDomainClaimedSent(gomock.Any(), orgID, userID).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: nil, + now: testNow, + }, + argsWorker{ + event: ¬ification.RequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: notification.RequestedType, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.UserDomainClaimedType, + MessageType: domain.DomainClaimedMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: login.LoginLink(eventOrigin, orgID), + CodeExpiry: 0, + Code: nil, + UnverifiedNotificationChannel: false, + IsOTP: false, + RequiresPreviousDomain: true, + Args: &domain.NotificationArguments{ + TempUsername: "tempUsername", + }, + }, + }, + }, w + }, + }, + { + name: "send failed, retry", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = &messages.Email{ + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + } + w.sendError = sendError + codeAlg, code := cryptoValue(t, ctrl, "testcode") + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().NotificationRetryRequested(gomock.Any(), gomock.Any(), notificationID, instanceID, + &command.NotificationRetryRequest{ + NotificationRequest: command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggerOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + BackOff: 1 * time.Second, + NotifyUser: &query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, + }, + sendError, + ).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + backOff: testBackOff, + maxAttempts: 2, + }, + argsWorker{ + event: ¬ification.RequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: notification.RequestedType, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + }, + }, w + }, + }, + { + name: "send failed (max attempts), cancel", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = &messages.Email{ + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + } + w.sendError = sendError + codeAlg, code := cryptoValue(t, ctrl, "testcode") + expectTemplateWithNotifyUserQueries(queries, givenTemplate) + commands.EXPECT().NotificationCanceled(gomock.Any(), gomock.Any(), notificationID, instanceID, sendError).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + backOff: testBackOff, + maxAttempts: 1, + }, + argsWorker{ + event: ¬ification.RequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Seq: 1, + Typ: notification.RequestedType, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + }, + }, w + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + queries := mock.NewMockQueries(ctrl) + commands := mock.NewMockCommands(ctrl) + f, a, w := tt.test(ctrl, queries, commands) + err := newNotificationWorker(t, ctrl, queries, f, a, w).reduceNotificationRequested( + authz.WithInstanceID(context.Background(), instanceID), + &sql.Tx{}, + a.event.(*notification.RequestedEvent)) + if w.err != nil { + w.err(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_userNotifier_reduceNotificationRetry(t *testing.T) { + testNow := time.Now + testBackOff := func(current time.Duration) time.Duration { + return time.Second + } + sendError := errors.New("send error") + tests := []struct { + name string + test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fieldsWorker, argsWorker, wantWorker) + }{ + { + name: "too old", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + codeAlg, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().NotificationCanceled(gomock.Any(), gomock.Any(), notificationID, instanceID, nil).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + }, + argsWorker{ + event: ¬ification.RetryRequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().Add(-1 * time.Hour), + Typ: notification.RequestedType, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + BackOff: 1 * time.Second, + NotifyUser: &query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, + }, + }, w + }, + }, + { + name: "backoff not done", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + codeAlg, code := cryptoValue(t, ctrl, "testcode") + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + }, + argsWorker{ + event: ¬ification.RetryRequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now(), + Typ: notification.RequestedType, + Seq: 2, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + BackOff: 10 * time.Second, + NotifyUser: &query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, + }, + }, w + }, + }, + { + name: "send ok", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = &messages.Email{ + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + } + codeAlg, code := cryptoValue(t, ctrl, "testcode") + expectTemplateQueries(queries, givenTemplate) + commands.EXPECT().NotificationSent(gomock.Any(), gomock.Any(), notificationID, instanceID).Return(nil) + commands.EXPECT().InviteCodeSent(gomock.Any(), orgID, userID).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + maxAttempts: 3, + }, + argsWorker{ + event: ¬ification.RetryRequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().Add(-2 * time.Second), + Typ: notification.RequestedType, + Seq: 2, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + BackOff: 1 * time.Second, + NotifyUser: &query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, + }, + }, w + }, + }, + { + name: "send failed, retry", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = &messages.Email{ + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + } + w.sendError = sendError + codeAlg, code := cryptoValue(t, ctrl, "testcode") + expectTemplateQueries(queries, givenTemplate) + commands.EXPECT().NotificationRetryRequested(gomock.Any(), gomock.Any(), notificationID, instanceID, + &command.NotificationRetryRequest{ + NotificationRequest: command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggerOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + BackOff: 1 * time.Second, + NotifyUser: &query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, + }, + sendError, + ).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + backOff: testBackOff, + maxAttempts: 3, + }, + argsWorker{ + event: ¬ification.RetryRequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().Add(-2 * time.Second), + Typ: notification.RequestedType, + Seq: 2, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + BackOff: 1 * time.Second, + NotifyUser: &query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, + }, + }, w + }, + }, + { + name: "send failed (max attempts), cancel", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) { + givenTemplate := "{{.LogoURL}}" + expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) + w.message = &messages.Email{ + Recipients: []string{lastEmail}, + Subject: "Invitation to APP", + Content: expectContent, + } + w.sendError = sendError + codeAlg, code := cryptoValue(t, ctrl, "testcode") + expectTemplateQueries(queries, givenTemplate) + commands.EXPECT().NotificationCanceled(gomock.Any(), gomock.Any(), notificationID, instanceID, sendError).Return(nil) + return fieldsWorker{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + userDataCrypto: codeAlg, + now: testNow, + backOff: testBackOff, + maxAttempts: 2, + }, + argsWorker{ + event: ¬ification.RetryRequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: notificationID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().Add(-2 * time.Second), + Seq: 2, + Typ: notification.RequestedType, + }), + Request: notification.Request{ + UserID: userID, + UserResourceOwner: orgID, + AggregateID: "", + AggregateResourceOwner: "", + TriggeredAtOrigin: eventOrigin, + EventType: user.HumanInviteCodeAddedType, + MessageType: domain.InviteUserMessageType, + NotificationType: domain.NotificationTypeEmail, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + CodeExpiry: 1 * time.Hour, + Code: code, + UnverifiedNotificationChannel: true, + IsOTP: false, + RequiresPreviousDomain: false, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + }, + }, + BackOff: 1 * time.Second, + NotifyUser: &query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, + }, + }, w + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + queries := mock.NewMockQueries(ctrl) + commands := mock.NewMockCommands(ctrl) + f, a, w := tt.test(ctrl, queries, commands) + err := newNotificationWorker(t, ctrl, queries, f, a, w).reduceNotificationRetry( + authz.WithInstanceID(context.Background(), instanceID), + &sql.Tx{}, + a.event.(*notification.RetryRequestedEvent)) + if w.err != nil { + w.err(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func newNotificationWorker(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fieldsWorker, a argsWorker, w wantWorker) *NotificationWorker { + queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) + smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") + channel := channel_mock.NewMockNotificationChannel(ctrl) + if w.err == nil { + if w.message != nil { + w.message.TriggeringEvent = a.event + channel.EXPECT().HandleMessage(w.message).Return(w.sendError) + } + if w.messageSMS != nil { + w.messageSMS.TriggeringEvent = a.event + channel.EXPECT().HandleMessage(w.messageSMS).DoAndReturn(func(message *messages.SMS) error { + message.VerificationID = gu.Ptr(verificationID) + return w.sendError + }) + } + } + return &NotificationWorker{ + commands: f.commands, + queries: NewNotificationQueries( + f.queries, + f.es, + externalDomain, + externalPort, + externalSecure, + "", + f.userDataCrypto, + smtpAlg, + f.SMSTokenCrypto, + ), + channels: ¬ificationChannels{ + Chain: *senders.ChainChannels(channel), + EmailConfig: &email.Config{ + ProviderConfig: &email.Provider{ + ID: "emailProviderID", + Description: "description", + }, + SMTPConfig: &smtp.Config{ + SMTP: smtp.SMTP{ + Host: "host", + User: "user", + Password: "password", + }, + Tls: true, + From: "from", + FromName: "fromName", + ReplyToAddress: "replyToAddress", + }, + WebhookConfig: nil, + }, + SMSConfig: &sms.Config{ + ProviderConfig: &sms.Provider{ + ID: "smsProviderID", + Description: "description", + }, + TwilioConfig: &twilio.Config{ + SID: "sid", + Token: "token", + SenderNumber: "senderNumber", + VerifyServiceSID: "verifyServiceSID", + }, + }, + }, + config: WorkerConfig{ + Workers: 1, + BulkLimit: 10, + RequeueEvery: 2 * time.Second, + HandleActiveInstances: 0, + TransactionDuration: 5 * time.Second, + MaxAttempts: f.maxAttempts, + MaxTtl: 5 * time.Minute, + MinRetryDelay: 1 * time.Second, + MaxRetryDelay: 10 * time.Second, + RetryDelayFactor: 2, + }, + now: f.now, + backOff: f.backOff, + } +} + +func TestNotificationWorker_exponentialBackOff(t *testing.T) { + type fields struct { + config WorkerConfig + } + type args struct { + current time.Duration + } + tests := []struct { + name string + fields fields + args args + wantMin time.Duration + wantMax time.Duration + }{ + { + name: "less than min, min - 1.5*min", + fields: fields{ + config: WorkerConfig{ + MinRetryDelay: 1 * time.Second, + MaxRetryDelay: 5 * time.Second, + RetryDelayFactor: 1.5, + }, + }, + args: args{ + current: 0, + }, + wantMin: 1000 * time.Millisecond, + wantMax: 1500 * time.Millisecond, + }, + { + name: "current, 1.5*current - max", + fields: fields{ + config: WorkerConfig{ + MinRetryDelay: 1 * time.Second, + MaxRetryDelay: 5 * time.Second, + RetryDelayFactor: 1.5, + }, + }, + args: args{ + current: 4 * time.Second, + }, + wantMin: 4000 * time.Millisecond, + wantMax: 5000 * time.Millisecond, + }, + { + name: "max, max", + fields: fields{ + config: WorkerConfig{ + MinRetryDelay: 1 * time.Second, + MaxRetryDelay: 5 * time.Second, + RetryDelayFactor: 1.5, + }, + }, + args: args{ + current: 5 * time.Second, + }, + wantMin: 5000 * time.Millisecond, + wantMax: 5000 * time.Millisecond, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &NotificationWorker{ + config: tt.fields.config, + } + b := w.exponentialBackOff(tt.args.current) + assert.GreaterOrEqual(t, b, tt.wantMin) + assert.LessOrEqual(t, b, tt.wantMax) + }) + } +} diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index 41d2f4dc8f..684c7b630d 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -2,23 +2,84 @@ package handlers import ( "context" - "strings" "time" http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/ui/console" "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/notification/senders" - "github.com/zitadel/zitadel/internal/notification/types" - "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) +func init() { + RegisterSentHandler(user.HumanInitialCodeAddedType, + func(ctx context.Context, commands Commands, id, orgID string, _ *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.HumanInitCodeSent(ctx, orgID, id) + }, + ) + RegisterSentHandler(user.HumanEmailCodeAddedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.HumanEmailVerificationCodeSent(ctx, orgID, id) + }, + ) + RegisterSentHandler(user.HumanPasswordCodeAddedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.PasswordCodeSent(ctx, orgID, id, generatorInfo) + }, + ) + RegisterSentHandler(user.HumanOTPSMSCodeAddedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.HumanOTPSMSCodeSent(ctx, id, orgID, generatorInfo) + }, + ) + RegisterSentHandler(session.OTPSMSChallengedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.OTPSMSSent(ctx, id, orgID, generatorInfo) + }, + ) + RegisterSentHandler(user.HumanOTPEmailCodeAddedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.HumanOTPEmailCodeSent(ctx, id, orgID) + }, + ) + RegisterSentHandler(session.OTPEmailChallengedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.OTPEmailSent(ctx, id, orgID) + }, + ) + RegisterSentHandler(user.UserDomainClaimedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.UserDomainClaimedSent(ctx, orgID, id) + }, + ) + RegisterSentHandler(user.HumanPasswordlessInitCodeRequestedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.HumanPasswordlessInitCodeSent(ctx, id, orgID, args["CodeID"].(string)) + }, + ) + RegisterSentHandler(user.HumanPasswordChangedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.PasswordChangeSent(ctx, orgID, id) + }, + ) + RegisterSentHandler(user.HumanPhoneCodeAddedType, + func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.HumanPhoneVerificationCodeSent(ctx, orgID, id, generatorInfo) + }, + ) + RegisterSentHandler(user.HumanInviteCodeAddedType, + func(ctx context.Context, commands Commands, id, orgID string, _ *senders.CodeGeneratorInfo, args map[string]any) error { + return commands.InviteCodeSent(ctx, orgID, id) + }, + ) +} + const ( UserNotificationsProjectionTable = "projections.notifications" ) @@ -26,7 +87,6 @@ const ( type userNotifier struct { commands Commands queries *NotificationQueries - channels types.ChannelChains otpEmailTmpl string } @@ -35,14 +95,12 @@ func NewUserNotifier( config handler.Config, commands Commands, queries *NotificationQueries, - channels types.ChannelChains, otpEmailTmpl string, ) *handler.Handler { return handler.NewHandler(ctx, &config, &userNotifier{ commands: commands, queries: queries, otpEmailTmpl: otpEmailTmpl, - channels: channels, }) } @@ -146,39 +204,29 @@ func (u *userNotifier) reduceInitCodeAdded(event eventstore.Event) (*handler.Sta if alreadyHandled { return nil } - code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto) - if err != nil { - return err - } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) - if err != nil { - return err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InitCodeMessageType) - if err != nil { - return err - } - ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). - SendUserInitCode(ctx, notifyUser, code, e.AuthRequestID) - if err != nil { - return err - } - return u.commands.HumanInitCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + origin := http_util.DomainContext(ctx).Origin() + return u.commands.RequestNotification( + ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + origin, + e.EventType, + domain.NotificationTypeEmail, + domain.InitCodeMessageType, + ). + WithURLTemplate(login.InitUserLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID)). + WithCode(e.Code, e.Expiry). + WithArgs(&domain.NotificationArguments{ + AuthRequestID: e.AuthRequestID, + }). + WithUnverifiedChannel(), + ) }), nil } @@ -203,42 +251,39 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St if alreadyHandled { return nil } - code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto) - if err != nil { - return err - } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) - if err != nil { - return err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyEmailMessageType) - if err != nil { - return err - } - ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). - SendEmailVerificationCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID) - if err != nil { - return err - } - return u.commands.HumanEmailVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + origin := http_util.DomainContext(ctx).Origin() + + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + origin, + e.EventType, + domain.NotificationTypeEmail, + domain.VerifyEmailMessageType, + ). + WithURLTemplate(u.emailCodeTemplate(origin, e)). + WithCode(e.Code, e.Expiry). + WithArgs(&domain.NotificationArguments{ + AuthRequestID: e.AuthRequestID, + }). + WithUnverifiedChannel(), + ) }), nil } +func (u *userNotifier) emailCodeTemplate(origin string, e *user.HumanEmailCodeAddedEvent) string { + if e.URLTemplate != "" { + return e.URLTemplate + } + return login.MailVerificationLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID) +} + func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.HumanPasswordCodeAddedEvent) if !ok { @@ -259,64 +304,74 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler if alreadyHandled { return nil } - var code string - if e.Code != nil { - code, err = crypto.DecryptString(e.Code, u.queries.UserDataCrypto) - if err != nil { - return err - } - } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) - if err != nil { - return err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordResetMessageType) - if err != nil { - return err - } - ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - generatorInfo := new(senders.CodeGeneratorInfo) - notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e) - if e.NotificationType == domain.NotificationTypeSms { - notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e, generatorInfo) - } - err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID) - if err != nil { - return err - } - return u.commands.PasswordCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID, generatorInfo) + origin := http_util.DomainContext(ctx).Origin() + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + origin, + e.EventType, + e.NotificationType, + domain.PasswordResetMessageType, + ). + WithURLTemplate(u.passwordCodeTemplate(origin, e)). + WithCode(e.Code, e.Expiry). + WithArgs(&domain.NotificationArguments{ + AuthRequestID: e.AuthRequestID, + }). + WithUnverifiedChannel(), + ) }), nil } +func (u *userNotifier) passwordCodeTemplate(origin string, e *user.HumanPasswordCodeAddedEvent) string { + if e.URLTemplate != "" { + return e.URLTemplate + } + return login.InitPasswordLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID) +} + func (u *userNotifier) reduceOTPSMSCodeAdded(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.HumanOTPSMSCodeAddedEvent) if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ASF3g", "reduce.wrong.event.type %s", user.HumanOTPSMSCodeAddedType) } - return u.reduceOTPSMS( - e, - e.Code, - e.Expiry, - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - u.commands.HumanOTPSMSCodeSent, - user.HumanOTPSMSCodeAddedType, - user.HumanOTPSMSCodeSentType, - ) + + return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error { + ctx := HandlerContext(event.Aggregate()) + alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + user.HumanOTPSMSCodeAddedType, + user.HumanOTPSMSCodeSentType) + if err != nil { + return err + } + if alreadyHandled { + return nil + } + ctx, err = u.queries.Origin(ctx, e) + if err != nil { + return err + } + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + http_util.DomainContext(ctx).Origin(), + e.EventType, + domain.NotificationTypeSms, + domain.VerifySMSOTPMessageType, + ). + WithCode(e.Code, e.Expiry). + WithArgs(otpArgs(ctx, e.Expiry)). + WithOTP(), + ) + }), nil } func (u *userNotifier) reduceSessionOTPSMSChallenged(event eventstore.Event) (*handler.Statement, error) { @@ -327,75 +382,46 @@ func (u *userNotifier) reduceSessionOTPSMSChallenged(event eventstore.Event) (*h if e.CodeReturned { return handler.NewNoOpStatement(e), nil } - ctx := HandlerContext(event.Aggregate()) - s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "") - if err != nil { - return nil, err - } - return u.reduceOTPSMS( - e, - e.Code, - e.Expiry, - s.UserFactor.UserID, - s.UserFactor.ResourceOwner, - u.commands.OTPSMSSent, - session.OTPSMSChallengedType, - session.OTPSMSSentType, - ) -} -func (u *userNotifier) reduceOTPSMS( - event eventstore.Event, - code *crypto.CryptoValue, - expiry time.Duration, - userID, - resourceOwner string, - sentCommand func(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) (err error), - eventTypes ...eventstore.EventType, -) (*handler.Statement, error) { - ctx := HandlerContext(event.Aggregate()) - alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, expiry, nil, eventTypes...) - if err != nil { - return nil, err - } - if alreadyHandled { - return handler.NewNoOpStatement(event), nil - } - var plainCode string - if code != nil { - plainCode, err = crypto.DecryptString(code, u.queries.UserDataCrypto) + return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error { + ctx := HandlerContext(event.Aggregate()) + alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + session.OTPSMSChallengedType, + session.OTPSMSSentType) if err != nil { - return nil, err + return err + } + if alreadyHandled { + return nil + } + s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "") + if err != nil { + return err } - } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, resourceOwner, false) - if err != nil { - return nil, err - } - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, userID) - if err != nil { - return nil, err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifySMSOTPMessageType) - if err != nil { - return nil, err - } - ctx, err = u.queries.Origin(ctx, event) - if err != nil { - return nil, err - } - generatorInfo := new(senders.CodeGeneratorInfo) - notify := types.SendSMS(ctx, u.channels, translator, notifyUser, colors, event, generatorInfo) - err = notify.SendOTPSMSCode(ctx, plainCode, expiry) - if err != nil { - return nil, err - } - err = sentCommand(ctx, event.Aggregate().ID, event.Aggregate().ResourceOwner, generatorInfo) - if err != nil { - return nil, err - } - return handler.NewNoOpStatement(event), nil + ctx, err = u.queries.Origin(ctx, e) + if err != nil { + return err + } + + args := otpArgs(ctx, e.Expiry) + args.SessionID = e.Aggregate().ID + return u.commands.RequestNotification(ctx, + s.UserFactor.ResourceOwner, + command.NewNotificationRequest( + s.UserFactor.UserID, + s.UserFactor.ResourceOwner, + http_util.DomainContext(ctx).Origin(), + e.EventType, + domain.NotificationTypeSms, + domain.VerifySMSOTPMessageType, + ). + WithAggregate(e.Aggregate().ID, e.Aggregate().ResourceOwner). + WithCode(e.Code, e.Expiry). + WithOTP(). + WithArgs(args), + ) + }), nil } func (u *userNotifier) reduceOTPEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) { @@ -403,24 +429,46 @@ func (u *userNotifier) reduceOTPEmailCodeAdded(event eventstore.Event) (*handler if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-JL3hw", "reduce.wrong.event.type %s", user.HumanOTPEmailCodeAddedType) } - var authRequestID string - if e.AuthRequestInfo != nil { - authRequestID = e.AuthRequestInfo.ID - } - url := func(code, origin string, _ *query.NotifyUser) (string, error) { - return login.OTPLink(origin, authRequestID, code, domain.MFATypeOTPEmail), nil - } - return u.reduceOTPEmail( - e, - e.Code, - e.Expiry, - e.Aggregate().ID, - e.Aggregate().ResourceOwner, - url, - u.commands.HumanOTPEmailCodeSent, - user.HumanOTPEmailCodeAddedType, - user.HumanOTPEmailCodeSentType, - ) + + return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error { + ctx := HandlerContext(event.Aggregate()) + alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + user.HumanOTPEmailCodeAddedType, + user.HumanOTPEmailCodeSentType) + if err != nil { + return err + } + if alreadyHandled { + return nil + } + + ctx, err = u.queries.Origin(ctx, e) + if err != nil { + return err + } + origin := http_util.DomainContext(ctx).Origin() + var authRequestID string + if e.AuthRequestInfo != nil { + authRequestID = e.AuthRequestInfo.ID + } + args := otpArgs(ctx, e.Expiry) + args.AuthRequestID = authRequestID + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + origin, + e.EventType, + domain.NotificationTypeEmail, + domain.VerifyEmailOTPMessageType, + ). + WithURLTemplate(login.OTPLinkTemplate(origin, authRequestID, domain.MFATypeOTPEmail)). + WithCode(e.Code, e.Expiry). + WithOTP(). + WithArgs(args), + ) + }), nil } func (u *userNotifier) reduceSessionOTPEmailChallenged(event eventstore.Event) (*handler.Statement, error) { @@ -431,93 +479,63 @@ func (u *userNotifier) reduceSessionOTPEmailChallenged(event eventstore.Event) ( if e.ReturnCode { return handler.NewNoOpStatement(e), nil } - ctx := HandlerContext(event.Aggregate()) - s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "") - if err != nil { - return nil, err - } - url := func(code, origin string, user *query.NotifyUser) (string, error) { - var buf strings.Builder - urlTmpl := origin + u.otpEmailTmpl - if e.URLTmpl != "" { - urlTmpl = e.URLTmpl + return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error { + ctx := HandlerContext(event.Aggregate()) + alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + session.OTPEmailChallengedType, + session.OTPEmailSentType) + if err != nil { + return err } - if err := domain.RenderOTPEmailURLTemplate(&buf, urlTmpl, code, user.ID, user.PreferredLoginName, user.DisplayName, e.Aggregate().ID, user.PreferredLanguage); err != nil { - return "", err + if alreadyHandled { + return nil } - return buf.String(), nil - } - return u.reduceOTPEmail( - e, - e.Code, - e.Expiry, - s.UserFactor.UserID, - s.UserFactor.ResourceOwner, - url, - u.commands.OTPEmailSent, - user.HumanOTPEmailCodeAddedType, - user.HumanOTPEmailCodeSentType, - ) + s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "") + if err != nil { + return err + } + + ctx, err = u.queries.Origin(ctx, e) + if err != nil { + return err + } + origin := http_util.DomainContext(ctx).Origin() + + args := otpArgs(ctx, e.Expiry) + args.SessionID = e.Aggregate().ID + return u.commands.RequestNotification(ctx, + s.UserFactor.ResourceOwner, + command.NewNotificationRequest( + s.UserFactor.UserID, + s.UserFactor.ResourceOwner, + origin, + e.EventType, + domain.NotificationTypeEmail, + domain.VerifyEmailOTPMessageType, + ). + WithAggregate(e.Aggregate().ID, e.Aggregate().ResourceOwner). + WithURLTemplate(u.otpEmailTemplate(origin, e)). + WithCode(e.Code, e.Expiry). + WithOTP(). + WithArgs(args), + ) + }), nil } -func (u *userNotifier) reduceOTPEmail( - event eventstore.Event, - code *crypto.CryptoValue, - expiry time.Duration, - userID, - resourceOwner string, - urlTmpl func(code, origin string, user *query.NotifyUser) (string, error), - sentCommand func(ctx context.Context, userID string, resourceOwner string) (err error), - eventTypes ...eventstore.EventType, -) (*handler.Statement, error) { - ctx := HandlerContext(event.Aggregate()) - alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, expiry, nil, eventTypes...) - if err != nil { - return nil, err - } - if alreadyHandled { - return handler.NewNoOpStatement(event), nil - } - plainCode, err := crypto.DecryptString(code, u.queries.UserDataCrypto) - if err != nil { - return nil, err - } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, resourceOwner, false) - if err != nil { - return nil, err +func (u *userNotifier) otpEmailTemplate(origin string, e *session.OTPEmailChallengedEvent) string { + if e.URLTmpl != "" { + return e.URLTmpl } + return origin + u.otpEmailTmpl +} - template, err := u.queries.MailTemplateByOrg(ctx, resourceOwner, false) - if err != nil { - return nil, err +func otpArgs(ctx context.Context, expiry time.Duration) *domain.NotificationArguments { + domainCtx := http_util.DomainContext(ctx) + return &domain.NotificationArguments{ + Origin: domainCtx.Origin(), + Domain: domainCtx.RequestedDomain(), + Expiry: expiry, } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, userID) - if err != nil { - return nil, err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, resourceOwner, domain.VerifyEmailOTPMessageType) - if err != nil { - return nil, err - } - ctx, err = u.queries.Origin(ctx, event) - if err != nil { - return nil, err - } - url, err := urlTmpl(plainCode, http_util.DomainContext(ctx).Origin(), notifyUser) - if err != nil { - return nil, err - } - notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event) - err = notify.SendOTPEmailCode(ctx, url, plainCode, expiry) - if err != nil { - return nil, err - } - err = sentCommand(ctx, event.Aggregate().ID, event.Aggregate().ResourceOwner) - if err != nil { - return nil, err - } - return handler.NewNoOpStatement(event), nil } func (u *userNotifier) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) { @@ -535,35 +553,28 @@ func (u *userNotifier) reduceDomainClaimed(event eventstore.Event) (*handler.Sta if alreadyHandled { return nil } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) - if err != nil { - return err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.DomainClaimedMessageType) - if err != nil { - return err - } - ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). - SendDomainClaimed(ctx, notifyUser, e.UserName) - if err != nil { - return err - } - return u.commands.UserDomainClaimedSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + origin := http_util.DomainContext(ctx).Origin() + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + origin, + e.EventType, + domain.NotificationTypeEmail, + domain.DomainClaimedMessageType, + ). + WithURLTemplate(login.LoginLink(origin, e.Aggregate().ResourceOwner)). + WithUnverifiedChannel(). + WithPreviousDomain(). + WithArgs(&domain.NotificationArguments{ + TempUsername: e.UserName, + }), + ) }), nil } @@ -585,42 +596,37 @@ func (u *userNotifier) reducePasswordlessCodeRequested(event eventstore.Event) ( if alreadyHandled { return nil } - code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto) - if err != nil { - return err - } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) - if err != nil { - return err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordlessRegistrationMessageType) - if err != nil { - return err - } - ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). - SendPasswordlessRegistrationLink(ctx, notifyUser, code, e.ID, e.URLTemplate) - if err != nil { - return err - } - return u.commands.HumanPasswordlessInitCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.ID) + origin := http_util.DomainContext(ctx).Origin() + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + origin, + e.EventType, + domain.NotificationTypeEmail, + domain.PasswordlessRegistrationMessageType, + ). + WithURLTemplate(u.passwordlessCodeTemplate(origin, e)). + WithCode(e.Code, e.Expiry). + WithArgs(&domain.NotificationArguments{ + CodeID: e.ID, + }), + ) }), nil } +func (u *userNotifier) passwordlessCodeTemplate(origin string, e *user.HumanPasswordlessInitCodeRequestedEvent) string { + if e.URLTemplate != "" { + return e.URLTemplate + } + return domain.PasswordlessInitCodeLinkTemplate(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.ID) +} + func (u *userNotifier) reducePasswordChanged(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.HumanPasswordChangedEvent) if !ok { @@ -638,10 +644,7 @@ func (u *userNotifier) reducePasswordChanged(event eventstore.Event) (*handler.S } notificationPolicy, err := u.queries.NotificationPolicyByOrg(ctx, true, e.Aggregate().ResourceOwner, false) - if zerrors.IsNotFound(err) { - return nil - } - if err != nil { + if err != nil && !zerrors.IsNotFound(err) { return err } @@ -649,34 +652,25 @@ func (u *userNotifier) reducePasswordChanged(event eventstore.Event) (*handler.S return nil } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) - if err != nil { - return err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordChangeMessageType) - if err != nil { - return err - } ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e). - SendPasswordChange(ctx, notifyUser) - if err != nil { - return err - } - return u.commands.PasswordChangeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + origin := http_util.DomainContext(ctx).Origin() + + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + origin, + e.EventType, + domain.NotificationTypeEmail, + domain.PasswordChangeMessageType, + ). + WithURLTemplate(console.LoginHintLink(origin, "{{.PreferredLoginName}}")). + WithUnverifiedChannel(), + ) }), nil } @@ -700,37 +694,28 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St if alreadyHandled { return nil } - var code string - if e.Code != nil { - code, err = crypto.DecryptString(e.Code, u.queries.UserDataCrypto) - if err != nil { - return err - } - } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) - if err != nil { - return err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyPhoneMessageType) - if err != nil { - return err - } ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - generatorInfo := new(senders.CodeGeneratorInfo) - if err = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e, generatorInfo). - SendPhoneVerificationCode(ctx, code); err != nil { - return err - } - return u.commands.HumanPhoneVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID, generatorInfo) + + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + http_util.DomainContext(ctx).Origin(), + e.EventType, + domain.NotificationTypeSms, + domain.VerifyPhoneMessageType, + ). + WithCode(e.Code, e.Expiry). + WithUnverifiedChannel(). + WithArgs(&domain.NotificationArguments{ + Domain: http_util.DomainContext(ctx).RequestedDomain(), + }), + ) }), nil } @@ -753,42 +738,45 @@ func (u *userNotifier) reduceInviteCodeAdded(event eventstore.Event) (*handler.S if alreadyHandled { return nil } - code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto) - if err != nil { - return err - } - colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) - if err != nil { - return err - } - - notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) - if err != nil { - return err - } - translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InviteUserMessageType) - if err != nil { - return err - } ctx, err = u.queries.Origin(ctx, e) if err != nil { return err } - notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e) - err = notify.SendInviteCode(ctx, notifyUser, code, e.ApplicationName, e.URLTemplate, e.AuthRequestID) - if err != nil { - return err + origin := http_util.DomainContext(ctx).Origin() + + applicationName := e.ApplicationName + if applicationName == "" { + applicationName = "ZITADEL" } - return u.commands.InviteCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner) + return u.commands.RequestNotification(ctx, + e.Aggregate().ResourceOwner, + command.NewNotificationRequest( + e.Aggregate().ID, + e.Aggregate().ResourceOwner, + origin, + e.EventType, + domain.NotificationTypeEmail, + domain.InviteUserMessageType, + ). + WithURLTemplate(u.inviteCodeTemplate(origin, e)). + WithCode(e.Code, e.Expiry). + WithUnverifiedChannel(). + WithArgs(&domain.NotificationArguments{ + AuthRequestID: e.AuthRequestID, + ApplicationName: applicationName, + }), + ) }), nil } +func (u *userNotifier) inviteCodeTemplate(origin string, e *user.HumanInviteCodeAddedEvent) string { + if e.URLTemplate != "" { + return e.URLTemplate + } + return login.InviteUserLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID) +} + func (u *userNotifier) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) { if expiry > 0 && event.CreatedAt().Add(expiry).Before(time.Now().UTC()) { return true, nil diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index 9692832787..b57edcc57c 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -7,22 +7,19 @@ import ( "testing" "time" - "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock" "github.com/zitadel/zitadel/internal/notification/channels/email" - channel_mock "github.com/zitadel/zitadel/internal/notification/channels/mock" "github.com/zitadel/zitadel/internal/notification/channels/set" "github.com/zitadel/zitadel/internal/notification/channels/sms" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/notification/channels/webhook" "github.com/zitadel/zitadel/internal/notification/handlers/mock" "github.com/zitadel/zitadel/internal/notification/messages" @@ -39,6 +36,8 @@ const ( userID = "user1" codeID = "event1" logoURL = "logo.png" + instanceID = "instanceID" + sessionID = "sessionID" eventOrigin = "https://triggered.here" eventOriginDomain = "triggered.here" assetsPath = "/assets/v1" @@ -60,196 +59,106 @@ const ( ) func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { - expectMailSubject := "Initialize User" tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanInitialCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + }{ + { + name: "with event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/user/init?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&passwordset={{.PasswordSet}}&authRequestID=%s", + eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInitialCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InitCodeMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &user.HumanInitialCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanInitialCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanInitialCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "without event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/user/init?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&passwordset={{.PasswordSet}}&authRequestID=%s", + externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInitialCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InitCodeMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - }, - }, w + }, args{ + event: &user.HumanInitialCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanInitialCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "button url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", eventOrigin, "", testCode, preferredLoginName, orgID, false, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanInitialCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - TriggeredAtOrigin: eventOrigin, - }, - }, w - }, - }, { - name: "button url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, preferredLoginName, orgID, false, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanInitialCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - }, - }, w - }, - }, { - name: "button url without event trigger url with authRequestID", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/user/init?authRequestID=%s&code=%s&loginname=%s&orgID=%s&passwordset=%t&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, preferredLoginName, orgID, false, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanInitCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanInitialCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - AuthRequestID: authRequestID, - }, - }, w - }, - }} - // TODO: Why don't we have an url template on user.HumanInitialCodeAddedEvent? + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) @@ -273,244 +182,141 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { } func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { - expectMailSubject := "Verify email" tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanEmailCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + }{ + { + name: "with event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/mail/verification?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanEmailCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &user.HumanEmailCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanEmailCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanEmailCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "without event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanEmailCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - }, - }, w + }, args{ + event: &user.HumanEmailCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanEmailCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "button url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", eventOrigin, "", testCode, orgID, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanEmailCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "return code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + w.noOperation = true + _, code := cryptoValue(t, ctrl, "testcode") + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &user.HumanEmailCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanEmailCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: true, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "button url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, orgID, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanEmailCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - }, - }, w - }, - }, { - name: "button url without event trigger url with authRequestID", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/mail/verification?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, orgID, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanEmailCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - AuthRequestID: authRequestID, - }, - }, w - }, - }, { - name: "button url with url template and event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" - testCode := "testcode" - expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanEmailVerificationCodeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanEmailCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - URLTemplate: urlTemplate, - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w - }, - }} + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) @@ -523,6 +329,10 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { } else { assert.NoError(t, err) } + if w.noOperation { + assert.Nil(t, stmt.Execute) + return + } err = stmt.Execute(nil, "") if w.err != nil { w.err(t, err) @@ -534,280 +344,189 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { } func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { - expectMailSubject := "Reset password" tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + }{ + { + name: "with event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanPasswordCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordResetMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "asset url without event trigger url", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanPasswordCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordResetMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - }, - }, w + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "button url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", eventOrigin, "", testCode, orgID, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "external code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/password/init?userID=%s&code={{.Code}}&orgID=%s&authRequestID=%s", + eventOrigin, userID, orgID, authRequestID), + Code: nil, + CodeExpiry: 0, + EventType: user.HumanPasswordCodeAddedType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.PasswordResetMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{AuthRequestID: authRequestID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordCodeAddedType, + }), + Code: nil, + Expiry: 0, + URLTemplate: "", + CodeReturned: false, + NotificationType: domain.NotificationTypeSms, + GeneratorID: smsProviderID, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "button url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, "", testCode, orgID, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "return code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + w.noOperation = true + _, code := cryptoValue(t, ctrl, "testcode") + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - }, - }, w + }, args{ + event: &user.HumanPasswordCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordCodeAddedType, + }), + Code: code, + Expiry: 1 * time.Hour, + URLTemplate: "", + CodeReturned: true, + NotificationType: domain.NotificationTypeSms, + GeneratorID: smsProviderID, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, }, - }, { - name: "button url without event trigger url with authRequestID", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/password/init?authRequestID=%s&code=%s&orgID=%s&userID=%s", externalProtocol, instancePrimaryDomain, externalPort, authRequestID, testCode, orgID, userID) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - AuthRequestID: authRequestID, - }, - }, w - }, - }, { - name: "button url with url template and event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" - testCode := "testcode" - expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - URLTemplate: urlTemplate, - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w - }, - }, { - name: "external code", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - expectContent := "We received a password reset request. Please use the button below to reset your password. (Code ) If you didn't ask for this mail, please ignore it." - w.messageSMS = &messages.SMS{ - SenderPhoneNumber: "senderNumber", - RecipientPhoneNumber: lastPhone, - Content: expectContent, - } - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordCodeSent(gomock.Any(), orgID, userID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordCodeAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: nil, - Expiry: 0, - URLTemplate: "", - CodeReturned: false, - NotificationType: domain.NotificationTypeSms, - GeneratorID: smsProviderID, - TriggeredAtOrigin: eventOrigin, - }, - }, w - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -821,6 +540,10 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { } else { assert.NoError(t, err) } + if w.noOperation { + assert.Nil(t, stmt.Execute) + return + } err = stmt.Execute(nil, "") if w.err != nil { w.err(t, err) @@ -832,22 +555,30 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { } func Test_userNotifier_reduceDomainClaimed(t *testing.T) { - expectMailSubject := "Domain has been claimed" tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) }{{ - name: "asset url with event trigger url", + name: "with event trigger", test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().UserDomainClaimedSent(gomock.Any(), orgID, userID).Return(nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/login?orgID=%s", + eventOrigin, orgID), + Code: nil, + CodeExpiry: 0, + EventType: user.UserDomainClaimedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.DomainClaimedMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{TempUsername: "newUsername"}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: true, + }).Return(nil) return fields{ queries: queries, commands: commands, @@ -857,32 +588,44 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { }, args{ event: &user.DomainClaimedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, AggregateID: userID, ResourceOwner: sql.NullString{String: orgID}, CreationDate: time.Now().UTC(), + Typ: user.UserDomainClaimedType, }), TriggeredAtOrigin: eventOrigin, + UserName: "newUsername", }, }, w }, }, { - name: "asset url without event trigger url", + name: "without event trigger", test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ Domains: []*query.InstanceDomain{{ Domain: instancePrimaryDomain, IsPrimary: true, }}, }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().UserDomainClaimedSent(gomock.Any(), orgID, userID).Return(nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/login?orgID=%s", + externalProtocol, instancePrimaryDomain, externalPort, orgID), + Code: nil, + CodeExpiry: 0, + EventType: user.UserDomainClaimedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.DomainClaimedMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{TempUsername: "newUsername"}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: true, + }).Return(nil) return fields{ queries: queries, commands: commands, @@ -892,10 +635,13 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { }, args{ event: &user.DomainClaimedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, AggregateID: userID, ResourceOwner: sql.NullString{String: orgID}, CreationDate: time.Now().UTC(), + Typ: user.UserDomainClaimedType, }), + UserName: "newUsername", }, }, w }, @@ -923,207 +669,138 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { } func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { - expectMailSubject := "Add Passwordless Login" tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanPasswordlessInitCodeSent(gomock.Any(), userID, orgID, codeID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordlessInitCodeRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + }{ + { + name: "with event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code={{.Code}}", eventOrigin, userID, orgID, codeID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanPasswordlessInitCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordlessRegistrationMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{CodeID: codeID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - ID: codeID, - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &user.HumanPasswordlessInitCodeRequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordlessInitCodeAddedType, + }), + ID: codeID, + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanPasswordlessInitCodeSent(gomock.Any(), userID, orgID, codeID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordlessInitCodeRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "without event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testCode") + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code={{.Code}}", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, codeID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanPasswordlessInitCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordlessRegistrationMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{CodeID: codeID}, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - ID: codeID, - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - }, - }, w + }, args{ + event: &user.HumanPasswordlessInitCodeRequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordlessInitCodeAddedType, + }), + ID: codeID, + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + }, + }, w + }, }, - }, { - name: "button url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectContent := fmt.Sprintf("%s/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code=%s", eventOrigin, userID, orgID, codeID, testCode) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanPasswordlessInitCodeSent(gomock.Any(), userID, orgID, codeID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordlessInitCodeRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "return code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + w.noOperation = true + _, code := cryptoValue(t, ctrl, "testcode") + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, }), - ID: codeID, - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &user.HumanPasswordlessInitCodeRequestedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordlessInitCodeAddedType, + }), + ID: codeID, + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: true, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, }, - }, { - name: "button url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectContent := fmt.Sprintf("%s://%s:%d/ui/login/login/passwordless/init?userID=%s&orgID=%s&codeID=%s&code=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, codeID, testCode) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanPasswordlessInitCodeSent(gomock.Any(), userID, orgID, codeID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &user.HumanPasswordlessInitCodeRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - ID: codeID, - Code: code, - Expiry: time.Hour, - URLTemplate: "", - CodeReturned: false, - }, - }, w - }, - }, { - name: "button url with url template and event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - urlTemplate := "https://my.custom.url/org/{{.OrgID}}/user/{{.UserID}}/verify/{{.Code}}" - testCode := "testcode" - expectContent := fmt.Sprintf("https://my.custom.url/org/%s/user/%s/verify/%s", orgID, userID, testCode) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().HumanPasswordlessInitCodeSent(gomock.Any(), userID, orgID, codeID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &user.HumanPasswordlessInitCodeRequestedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - ID: codeID, - Code: code, - Expiry: time.Hour, - URLTemplate: urlTemplate, - CodeReturned: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w - }, - }} + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) @@ -1136,6 +813,10 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { } else { assert.NoError(t, err) } + if w.noOperation { + assert.Nil(t, stmt.Execute) + return + } err = stmt.Execute(nil, "") if w.err != nil { w.err(t, err) @@ -1147,80 +828,127 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { } func Test_userNotifier_reducePasswordChanged(t *testing.T) { - expectMailSubject := "Password of user has changed" tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{ - PasswordChange: true, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordChangeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - }, args{ - event: &user.HumanPasswordChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + }{ + { + name: "with event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{ + PasswordChange: true, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/console?login_hint={{.PreferredLoginName}}", eventOrigin), + Code: nil, + CodeExpiry: 0, + EventType: user.HumanPasswordChangedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordChangeMessageType, + UnverifiedNotificationChannel: true, + Args: nil, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &user.HumanPasswordChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordChangedType, + }), + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{lastEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{ - PasswordChange: true, - }, nil) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - commands.EXPECT().PasswordChangeSent(gomock.Any(), orgID, userID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - }, args{ - event: &user.HumanPasswordChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "without event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{ + PasswordChange: true, + }, nil) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/console?login_hint={{.PreferredLoginName}}", + externalProtocol, instancePrimaryDomain, externalPort), + Code: nil, + CodeExpiry: 0, + EventType: user.HumanPasswordChangedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.PasswordChangeMessageType, + UnverifiedNotificationChannel: true, + Args: nil, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - }, - }, w + }, args{ + event: &user.HumanPasswordChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordChangedType, + }), + }, + }, w + }, + }, { + name: "no notification", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + queries.EXPECT().NotificationPolicyByOrg(gomock.Any(), gomock.Any(), orgID, gomock.Any()).Return(&query.NotificationPolicy{ + PasswordChange: false, + }, nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &user.HumanPasswordChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanPasswordChangedType, + }), + }, + }, w + }, }, - }} + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) @@ -1244,213 +972,235 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { } func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { - expectMailSubject := "Verify One-Time Password" tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{verifiedEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) - commands.EXPECT().OTPEmailSent(gomock.Any(), userID, orgID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &session.OTPEmailChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - URLTmpl: "", - ReturnCode: false, - TriggeredAtOrigin: eventOrigin, + }{ + { + name: "url with event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testCode") + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{ + ID: sessionID, + ResourceOwner: instanceID, + UserFactor: query.SessionUserFactor{ + UserID: userID, + ResourceOwner: orgID, }, - }, w - }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - expectContent := fmt.Sprintf("%s://%s:%d%s/%s/%s", externalProtocol, instancePrimaryDomain, externalPort, assetsPath, policyID, logoURL) - w.message = &messages.Email{ - Recipients: []string{verifiedEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, "testcode") - expectTemplateQueries(queries, givenTemplate) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - commands.EXPECT().OTPEmailSent(gomock.Any(), userID, orgID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &session.OTPEmailChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: code, - Expiry: time.Hour, - URLTmpl: "", - ReturnCode: false, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/otp/verify?loginName={{.LoginName}}&code={{.Code}}", eventOrigin), + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPEmailChallengedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: eventOriginDomain, + Expiry: 1 * time.Hour, + Origin: eventOrigin, + SessionID: sessionID, }, - }, w - }, - }, { - name: "button url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s/otp/verify?loginName=%s&code=%s", eventOrigin, preferredLoginName, testCode) - w.message = &messages.Email{ - Recipients: []string{verifiedEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateQueries(queries, givenTemplate) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) - commands.EXPECT().OTPEmailSent(gomock.Any(), userID, orgID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &session.OTPEmailChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + AggregateID: sessionID, + AggregateResourceOwner: instanceID, + IsOTP: true, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - URLTmpl: "", - ReturnCode: false, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &session.OTPEmailChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: sessionID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: session.OTPEmailChallengedType, + }), + Code: code, + Expiry: time.Hour, + URLTmpl: "", + ReturnCode: false, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, }, - }, { - name: "button url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - testCode := "testcode" - expectContent := fmt.Sprintf("%s://%s:%d/otp/verify?loginName=%s&code=%s", externalProtocol, instancePrimaryDomain, externalPort, preferredLoginName, testCode) - w.message = &messages.Email{ - Recipients: []string{verifiedEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - expectTemplateQueries(queries, givenTemplate) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) - commands.EXPECT().OTPEmailSent(gomock.Any(), userID, orgID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - }, args{ - event: &session.OTPEmailChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "without event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testCode") + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{ + ID: sessionID, + ResourceOwner: instanceID, + UserFactor: query.SessionUserFactor{ + UserID: userID, + ResourceOwner: orgID, + }, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/otp/verify?loginName={{.LoginName}}&code={{.Code}}", externalProtocol, instancePrimaryDomain, externalPort), + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPEmailChallengedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: instancePrimaryDomain, + Expiry: 1 * time.Hour, + Origin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + SessionID: sessionID, + }, + AggregateID: sessionID, + AggregateResourceOwner: instanceID, + IsOTP: true, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, }), - Code: code, - Expiry: time.Hour, - ReturnCode: false, - }, - }, w + }, args{ + event: &session.OTPEmailChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: sessionID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: session.OTPEmailChallengedType, + }), + Code: code, + Expiry: time.Hour, + ReturnCode: false, + }, + }, w + }, }, - }, { - name: "button url with url template and event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.URL}}" - urlTemplate := "https://my.custom.url/user/{{.LoginName}}/verify" - testCode := "testcode" - expectContent := fmt.Sprintf("https://my.custom.url/user/%s/verify", preferredLoginName) - w.message = &messages.Email{ - Recipients: []string{verifiedEmail}, - Subject: expectMailSubject, - Content: expectContent, - } - codeAlg, code := cryptoValue(t, ctrl, testCode) - expectTemplateQueries(queries, givenTemplate) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) - commands.EXPECT().OTPEmailSent(gomock.Any(), userID, orgID).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - userDataCrypto: codeAlg, - SMSTokenCrypto: nil, - }, args{ - event: &session.OTPEmailChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), + { + name: "return code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + w.noOperation = true + _, code := cryptoValue(t, ctrl, "testCode") + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, }), - Code: code, - Expiry: time.Hour, - ReturnCode: false, - URLTmpl: urlTemplate, - TriggeredAtOrigin: eventOrigin, - }, - }, w + }, args{ + event: &session.OTPEmailChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: sessionID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: session.OTPEmailChallengedType, + }), + Code: code, + Expiry: time.Hour, + URLTmpl: "", + ReturnCode: true, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, }, - }} + { + name: "url template", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testCode") + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{ + ID: sessionID, + ResourceOwner: instanceID, + UserFactor: query.SessionUserFactor{ + UserID: userID, + ResourceOwner: orgID, + }, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: "/verify-otp?sessionID={{.SessionID}}", + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPEmailChallengedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.VerifyEmailOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: eventOriginDomain, + Expiry: 1 * time.Hour, + Origin: eventOrigin, + SessionID: sessionID, + }, + AggregateID: sessionID, + AggregateResourceOwner: instanceID, + IsOTP: true, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPEmailChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: sessionID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: session.OTPEmailChallengedType, + }), + Code: code, + Expiry: time.Hour, + URLTmpl: "/verify-otp?sessionID={{.SessionID}}", + ReturnCode: false, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - _, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPEmailChallenged(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPEmailChallenged(a.event) + if w.err != nil { + w.err(t, err) + } else { + assert.NoError(t, err) + } + if w.noOperation { + assert.Nil(t, stmt.Execute) + return + } + err = stmt.Execute(nil, "") if w.err != nil { w.err(t, err) } else { @@ -1464,86 +1214,212 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { tests := []struct { name string test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) - }{{ - name: "asset url with event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - testCode := "" - expiry := 0 * time.Hour - expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. -@%[2]s #%[1]s`, testCode, eventOriginDomain, expiry) - w.messageSMS = &messages.SMS{ - SenderPhoneNumber: "senderNumber", - RecipientPhoneNumber: verifiedPhone, - Content: expectContent, - } - expectTemplateQueriesSMS(queries, givenTemplate) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) - commands.EXPECT().OTPSMSSent(gomock.Any(), userID, orgID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - }, args{ - event: &session.OTPSMSChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: nil, - Expiry: expiry, - CodeReturned: false, - GeneratorID: smsProviderID, - TriggeredAtOrigin: eventOrigin, + }{ + { + name: "with event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + testCode := "testcode" + _, code := cryptoValue(t, ctrl, testCode) + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{ + ID: sessionID, + ResourceOwner: instanceID, + UserFactor: query.SessionUserFactor{ + UserID: userID, + ResourceOwner: orgID, }, - }, w - }, - }, { - name: "asset url without event trigger url", - test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { - givenTemplate := "{{.LogoURL}}" - testCode := "" - expiry := 0 * time.Hour - expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s. -@%[2]s #%[1]s`, testCode, instancePrimaryDomain, expiry) - w.messageSMS = &messages.SMS{ - SenderPhoneNumber: "senderNumber", - RecipientPhoneNumber: verifiedPhone, - Content: expectContent, - } - expectTemplateQueriesSMS(queries, givenTemplate) - queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), userID, gomock.Any()).Return(&query.Session{}, nil) - queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ - Domains: []*query.InstanceDomain{{ - Domain: instancePrimaryDomain, - IsPrimary: true, - }}, - }, nil) - commands.EXPECT().OTPSMSSent(gomock.Any(), userID, orgID, &senders.CodeGeneratorInfo{ID: smsProviderID, VerificationID: verificationID}).Return(nil) - return fields{ - queries: queries, - commands: commands, - es: eventstore.NewEventstore(&eventstore.Config{ - Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, - }), - }, args{ - event: &session.OTPSMSChallengedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ - AggregateID: userID, - ResourceOwner: sql.NullString{String: orgID}, - CreationDate: time.Now().UTC(), - }), - Code: nil, - Expiry: expiry, - CodeReturned: false, - GeneratorID: smsProviderID, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: "", + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPSMSChallengedType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifySMSOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: eventOriginDomain, + Expiry: 1 * time.Hour, + Origin: eventOrigin, + SessionID: sessionID, }, - }, w + AggregateID: sessionID, + AggregateResourceOwner: instanceID, + IsOTP: true, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: sessionID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: session.OTPSMSChallengedType, + }), + Code: code, + Expiry: 1 * time.Hour, + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, + { + name: "without event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + testCode := "testcode" + _, code := cryptoValue(t, ctrl, testCode) + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{ + ID: sessionID, + ResourceOwner: instanceID, + UserFactor: query.SessionUserFactor{ + UserID: userID, + ResourceOwner: orgID, + }, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: "", + Code: code, + CodeExpiry: time.Hour, + EventType: session.OTPSMSChallengedType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifySMSOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: instancePrimaryDomain, + Expiry: 1 * time.Hour, + Origin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + SessionID: sessionID, + }, + AggregateID: sessionID, + AggregateResourceOwner: instanceID, + IsOTP: true, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: sessionID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: session.OTPSMSChallengedType, + }), + Code: code, + Expiry: 1 * time.Hour, + CodeReturned: false, + }, + }, w + }, + }, + { + name: "external code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + queries.EXPECT().SessionByID(gomock.Any(), gomock.Any(), sessionID, gomock.Any()).Return(&query.Session{ + ID: sessionID, + ResourceOwner: instanceID, + UserFactor: query.SessionUserFactor{ + UserID: userID, + ResourceOwner: orgID, + }, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: "", + Code: nil, + CodeExpiry: 0, + EventType: session.OTPSMSChallengedType, + NotificationType: domain.NotificationTypeSms, + MessageType: domain.VerifySMSOTPMessageType, + UnverifiedNotificationChannel: false, + Args: &domain.NotificationArguments{ + Domain: eventOriginDomain, + Expiry: 0 * time.Hour, + Origin: eventOrigin, + SessionID: sessionID, + }, + AggregateID: sessionID, + AggregateResourceOwner: instanceID, + IsOTP: true, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: sessionID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: session.OTPSMSChallengedType, + }), + Code: nil, + Expiry: 0, + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, + }, + { + name: "return code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + w.noOperation = true + _, code := cryptoValue(t, ctrl, "testCode") + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + }, args{ + event: &session.OTPSMSChallengedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: sessionID, + ResourceOwner: sql.NullString{String: instanceID}, + CreationDate: time.Now().UTC(), + Typ: session.OTPSMSChallengedType, + }), + Code: code, + Expiry: 1 * time.Hour, + CodeReturned: true, + TriggeredAtOrigin: eventOrigin, + }, + }, w + }, }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1551,7 +1427,281 @@ func Test_userNotifier_reduceOTPSMSChallenged(t *testing.T) { queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - _, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPSMSChallenged(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPSMSChallenged(a.event) + if w.err != nil { + w.err(t, err) + } else { + assert.NoError(t, err) + } + if w.noOperation { + assert.Nil(t, stmt.Execute) + return + } + err = stmt.Execute(nil, "") + if w.err != nil { + w.err(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_userNotifier_reduceInviteCodeAdded(t *testing.T) { + tests := []struct { + name string + test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fields, args, want) + }{ + { + name: "with event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInviteCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ + ApplicationName: "ZITADEL", + AuthRequestID: authRequestID, + }, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &user.HumanInviteCodeAddedEvent{ + BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanInviteCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, + }, + { + name: "without event trigger", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testCode") + queries.EXPECT().SearchInstanceDomains(gomock.Any(), gomock.Any()).Return(&query.InstanceDomains{ + Domains: []*query.InstanceDomain{{ + Domain: instancePrimaryDomain, + IsPrimary: true, + }}, + }, nil) + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: fmt.Sprintf("%s://%s:%d", externalProtocol, instancePrimaryDomain, externalPort), + URLTemplate: fmt.Sprintf("%s://%s:%d/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", externalProtocol, instancePrimaryDomain, externalPort, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInviteCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ + ApplicationName: "ZITADEL", + AuthRequestID: authRequestID, + }, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &user.HumanInviteCodeAddedEvent{ + BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanInviteCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + AuthRequestID: authRequestID, + }, + }, w + }, + }, + { + name: "return code", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + w.noOperation = true + _, code := cryptoValue(t, ctrl, "testcode") + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).MockQuerier, + }), + }, args{ + event: &user.HumanInviteCodeAddedEvent{ + BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanInviteCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: true, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, + }, + { + name: "url template", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: "/passwordless-init?userID={{.UserID}}", + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInviteCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ + ApplicationName: "ZITADEL", + AuthRequestID: authRequestID, + }, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &user.HumanInviteCodeAddedEvent{ + BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanInviteCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "/passwordless-init?userID={{.UserID}}", + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + }, + }, w + }, + }, + { + name: "application name", + test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fields, a args, w want) { + _, code := cryptoValue(t, ctrl, "testcode") + commands.EXPECT().RequestNotification(gomock.Any(), orgID, &command.NotificationRequest{ + UserID: userID, + UserResourceOwner: orgID, + TriggerOrigin: eventOrigin, + URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID), + Code: code, + CodeExpiry: time.Hour, + EventType: user.HumanInviteCodeAddedType, + NotificationType: domain.NotificationTypeEmail, + MessageType: domain.InviteUserMessageType, + UnverifiedNotificationChannel: true, + Args: &domain.NotificationArguments{ + ApplicationName: "APP", + AuthRequestID: authRequestID, + }, + AggregateID: "", + AggregateResourceOwner: "", + IsOTP: false, + RequiresPreviousDomain: false, + }).Return(nil) + return fields{ + queries: queries, + commands: commands, + es: eventstore.NewEventstore(&eventstore.Config{ + Querier: es_repo_mock.NewRepo(t).ExpectFilterEvents().MockQuerier, + }), + }, args{ + event: &user.HumanInviteCodeAddedEvent{ + BaseEvent: eventstore.BaseEventFromRepo(&repository.Event{ + InstanceID: instanceID, + AggregateID: userID, + ResourceOwner: sql.NullString{String: orgID}, + CreationDate: time.Now().UTC(), + Typ: user.HumanInviteCodeAddedType, + }), + Code: code, + Expiry: time.Hour, + URLTemplate: "", + CodeReturned: false, + TriggeredAtOrigin: eventOrigin, + AuthRequestID: authRequestID, + ApplicationName: "APP", + }, + }, w + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + queries := mock.NewMockQueries(ctrl) + commands := mock.NewMockCommands(ctrl) + f, a, w := tt.test(ctrl, queries, commands) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceInviteCodeAdded(a.event) + if w.err != nil { + w.err(t, err) + } else { + assert.NoError(t, err) + } + if w.noOperation { + assert.Nil(t, stmt.Execute) + return + } + err = stmt.Execute(nil, "") if w.err != nil { w.err(t, err) } else { @@ -1568,32 +1718,36 @@ type fields struct { userDataCrypto crypto.EncryptionAlgorithm SMSTokenCrypto crypto.EncryptionAlgorithm } +type fieldsWorker struct { + queries *mock.MockQueries + commands *mock.MockCommands + es *eventstore.Eventstore + userDataCrypto crypto.EncryptionAlgorithm + SMSTokenCrypto crypto.EncryptionAlgorithm + now nowFunc + backOff func(current time.Duration) time.Duration + maxAttempts uint8 +} type args struct { event eventstore.Event } +type argsWorker struct { + event eventstore.Event +} type want struct { + noOperation bool + err assert.ErrorAssertionFunc +} +type wantWorker struct { message *messages.Email messageSMS *messages.SMS + sendError error err assert.ErrorAssertionFunc } func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fields, a args, w want) *userNotifier { queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") - channel := channel_mock.NewMockNotificationChannel(ctrl) - if w.err == nil { - if w.message != nil { - w.message.TriggeringEvent = a.event - channel.EXPECT().HandleMessage(w.message).Return(nil) - } - if w.messageSMS != nil { - w.messageSMS.TriggeringEvent = a.event - channel.EXPECT().HandleMessage(w.messageSMS).DoAndReturn(func(message *messages.SMS) error { - message.VerificationID = gu.Ptr(verificationID) - return nil - }) - } - } return &userNotifier{ commands: f.commands, queries: NewNotificationQueries( @@ -1608,63 +1762,30 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu f.SMSTokenCrypto, ), otpEmailTmpl: defaultOTPEmailTemplate, - channels: &channels{ - Chain: *senders.ChainChannels(channel), - EmailConfig: &email.Config{ - ProviderConfig: &email.Provider{ - ID: "emailProviderID", - Description: "description", - }, - SMTPConfig: &smtp.Config{ - SMTP: smtp.SMTP{ - Host: "host", - User: "user", - Password: "password", - }, - Tls: true, - From: "from", - FromName: "fromName", - ReplyToAddress: "replyToAddress", - }, - WebhookConfig: nil, - }, - SMSConfig: &sms.Config{ - ProviderConfig: &sms.Provider{ - ID: "smsProviderID", - Description: "description", - }, - TwilioConfig: &twilio.Config{ - SID: "sid", - Token: "token", - SenderNumber: "senderNumber", - VerifyServiceSID: "verifyServiceSID", - }, - }, - }, } } -var _ types.ChannelChains = (*channels)(nil) +var _ types.ChannelChains = (*notificationChannels)(nil) -type channels struct { +type notificationChannels struct { senders.Chain EmailConfig *email.Config SMSConfig *sms.Config } -func (c *channels) Email(context.Context) (*senders.Chain, *email.Config, error) { +func (c *notificationChannels) Email(context.Context) (*senders.Chain, *email.Config, error) { return &c.Chain, c.EmailConfig, nil } -func (c *channels) SMS(context.Context) (*senders.Chain, *sms.Config, error) { +func (c *notificationChannels) SMS(context.Context) (*senders.Chain, *sms.Config, error) { return &c.Chain, c.SMSConfig, nil } -func (c *channels) Webhook(context.Context, webhook.Config) (*senders.Chain, error) { +func (c *notificationChannels) Webhook(context.Context, webhook.Config) (*senders.Chain, error) { return &c.Chain, nil } -func (c *channels) SecurityTokenEvent(context.Context, set.Config) (*senders.Chain, error) { +func (c *notificationChannels) SecurityTokenEvent(context.Context, set.Config) (*senders.Chain, error) { return &c.Chain, nil } @@ -1679,6 +1800,11 @@ func expectTemplateQueries(queries *mock.MockQueries, template string) { }, }, nil) queries.EXPECT().MailTemplateByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.MailTemplate{Template: []byte(template)}, nil) + queries.EXPECT().GetDefaultLanguage(gomock.Any()).Return(language.English) + queries.EXPECT().CustomTextListByTemplate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(&query.CustomTexts{}, nil) +} + +func expectTemplateWithNotifyUserQueries(queries *mock.MockQueries, template string) { queries.EXPECT().GetNotifyUserByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.NotifyUser{ ID: userID, ResourceOwner: orgID, @@ -1688,11 +1814,19 @@ func expectTemplateQueries(queries *mock.MockQueries, template string) { LastPhone: lastPhone, VerifiedPhone: verifiedPhone, }, nil) - queries.EXPECT().GetDefaultLanguage(gomock.Any()).Return(language.English) - queries.EXPECT().CustomTextListByTemplate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(&query.CustomTexts{}, nil) + expectTemplateQueries(queries, template) } -func expectTemplateQueriesSMS(queries *mock.MockQueries, template string) { +func expectTemplateWithNotifyUserQueriesSMS(queries *mock.MockQueries) { + queries.EXPECT().GetNotifyUserByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.NotifyUser{ + ID: userID, + ResourceOwner: orgID, + LastEmail: lastEmail, + VerifiedEmail: verifiedEmail, + PreferredLoginName: preferredLoginName, + LastPhone: lastPhone, + VerifiedPhone: verifiedPhone, + }, nil) queries.EXPECT().GetInstanceRestrictions(gomock.Any()).Return(query.Restrictions{ AllowedLanguages: []language.Tag{language.English}, }, nil) @@ -1702,15 +1836,6 @@ func expectTemplateQueriesSMS(queries *mock.MockQueries, template string) { LogoURL: logoURL, }, }, nil) - queries.EXPECT().GetNotifyUserByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.NotifyUser{ - ID: userID, - ResourceOwner: orgID, - LastEmail: lastEmail, - VerifiedEmail: verifiedEmail, - PreferredLoginName: preferredLoginName, - LastPhone: lastPhone, - VerifiedPhone: verifiedPhone, - }, nil) queries.EXPECT().GetDefaultLanguage(gomock.Any()).Return(language.English) queries.EXPECT().CustomTextListByTemplate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(&query.CustomTexts{}, nil) } diff --git a/internal/notification/projections.go b/internal/notification/projections.go index 2be95f1490..1a8c70cd40 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/notification/handlers" @@ -14,11 +15,15 @@ import ( "github.com/zitadel/zitadel/internal/query/projection" ) -var projections []*handler.Handler +var ( + projections []*handler.Handler + worker *handlers.NotificationWorker +) func Register( ctx context.Context, userHandlerCustomConfig, quotaHandlerCustomConfig, telemetryHandlerCustomConfig, backChannelLogoutHandlerCustomConfig projection.CustomConfig, + notificationWorkerConfig handlers.WorkerConfig, telemetryCfg handlers.TelemetryPusherConfig, externalDomain string, externalPort uint16, @@ -29,10 +34,11 @@ func Register( otpEmailTmpl, fileSystemPath string, userEncryption, smtpEncryption, smsEncryption, keysEncryptionAlg crypto.EncryptionAlgorithm, tokenLifetime time.Duration, + client *database.DB, ) { q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption) c := newChannels(q) - projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl)) + projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, otpEmailTmpl)) projections = append(projections, handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c)) projections = append(projections, handlers.NewBackChannelLogoutNotifier( ctx, @@ -47,12 +53,14 @@ func Register( if telemetryCfg.Enabled { projections = append(projections, handlers.NewTelemetryPusher(ctx, telemetryCfg, projection.ApplyCustomConfig(telemetryHandlerCustomConfig), commands, q, c)) } + worker = handlers.NewNotificationWorker(notificationWorkerConfig, commands, q, es, client, c) } func Start(ctx context.Context) { for _, projection := range projections { projection.Start(ctx) } + worker.Start(ctx) } func ProjectInstance(ctx context.Context) error { diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go deleted file mode 100644 index 433728392b..0000000000 --- a/internal/notification/types/domain_claimed.go +++ /dev/null @@ -1,20 +0,0 @@ -package types - -import ( - "context" - "strings" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" -) - -func (notify Notify) SendDomainClaimed(ctx context.Context, user *query.NotifyUser, username string) error { - url := login.LoginLink(http_utils.DomainContext(ctx).Origin(), user.ResourceOwner) - index := strings.LastIndex(user.LastEmail, "@") - args := make(map[string]interface{}) - args["TempUsername"] = username - args["Domain"] = user.LastEmail[index+1:] - return notify(url, args, domain.DomainClaimedMessageType, true) -} diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go deleted file mode 100644 index 4ff59137b1..0000000000 --- a/internal/notification/types/email_verification_code.go +++ /dev/null @@ -1,28 +0,0 @@ -package types - -import ( - "context" - "strings" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" -) - -func (notify Notify) SendEmailVerificationCode(ctx context.Context, user *query.NotifyUser, code string, urlTmpl, authRequestID string) error { - var url string - if urlTmpl == "" { - url = login.MailVerificationLink(http_utils.DomainContext(ctx).Origin(), user.ID, code, user.ResourceOwner, authRequestID) - } else { - var buf strings.Builder - if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { - return err - } - url = buf.String() - } - - args := make(map[string]interface{}) - args["Code"] = code - return notify(url, args, domain.VerifyEmailMessageType, true) -} diff --git a/internal/notification/types/email_verification_code_test.go b/internal/notification/types/email_verification_code_test.go deleted file mode 100644 index 2196e25b0c..0000000000 --- a/internal/notification/types/email_verification_code_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package types - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" -) - -func TestNotify_SendEmailVerificationCode(t *testing.T) { - type args struct { - user *query.NotifyUser - origin *http_utils.DomainCtx - code string - urlTmpl string - authRequestID string - } - tests := []struct { - name string - args args - want *notifyResult - wantErr error - }{ - { - name: "default URL", - args: args{ - user: &query.NotifyUser{ - ID: "user1", - ResourceOwner: "org1", - }, - origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, - code: "123", - urlTmpl: "", - authRequestID: "authRequestID", - }, - want: ¬ifyResult{ - url: "https://example.com/ui/login/mail/verification?authRequestID=authRequestID&code=123&orgID=org1&userID=user1", - args: map[string]interface{}{"Code": "123"}, - messageType: domain.VerifyEmailMessageType, - allowUnverifiedNotificationChannel: true, - }, - }, - { - name: "template error", - args: args{ - user: &query.NotifyUser{ - ID: "user1", - ResourceOwner: "org1", - }, - origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, - code: "123", - urlTmpl: "{{", - authRequestID: "authRequestID", - }, - want: ¬ifyResult{}, - wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"), - }, - { - name: "template success", - args: args{ - user: &query.NotifyUser{ - ID: "user1", - ResourceOwner: "org1", - }, - origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, - code: "123", - urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", - authRequestID: "authRequestID", - }, - want: ¬ifyResult{ - url: "https://example.com/email/verify?userID=user1&code=123&orgID=org1", - args: map[string]interface{}{"Code": "123"}, - messageType: domain.VerifyEmailMessageType, - allowUnverifiedNotificationChannel: true, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, notify := mockNotify() - err := notify.SendEmailVerificationCode(http_utils.WithDomainContext(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.urlTmpl, tt.args.authRequestID) - require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/internal/notification/types/init_code.go b/internal/notification/types/init_code.go deleted file mode 100644 index 3e38cc284b..0000000000 --- a/internal/notification/types/init_code.go +++ /dev/null @@ -1,17 +0,0 @@ -package types - -import ( - "context" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" -) - -func (notify Notify) SendUserInitCode(ctx context.Context, user *query.NotifyUser, code, authRequestID string) error { - url := login.InitUserLink(http_utils.DomainContext(ctx).Origin(), user.ID, user.PreferredLoginName, code, user.ResourceOwner, user.PasswordSet, authRequestID) - args := make(map[string]interface{}) - args["Code"] = code - return notify(url, args, domain.InitCodeMessageType, true) -} diff --git a/internal/notification/types/invite_code.go b/internal/notification/types/invite_code.go deleted file mode 100644 index 953124a553..0000000000 --- a/internal/notification/types/invite_code.go +++ /dev/null @@ -1,31 +0,0 @@ -package types - -import ( - "context" - "strings" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" -) - -func (notify Notify) SendInviteCode(ctx context.Context, user *query.NotifyUser, code, applicationName, urlTmpl, authRequestID string) error { - var url string - if applicationName == "" { - applicationName = "ZITADEL" - } - if urlTmpl == "" { - url = login.InviteUserLink(http_utils.DomainContext(ctx).Origin(), user.ID, user.PreferredLoginName, code, user.ResourceOwner, authRequestID) - } else { - var buf strings.Builder - if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { - return err - } - url = buf.String() - } - args := make(map[string]interface{}) - args["Code"] = code - args["ApplicationName"] = applicationName - return notify(url, args, domain.InviteUserMessageType, true) -} diff --git a/internal/notification/types/notification.go b/internal/notification/types/notification.go index 61c4cf70de..db791851bc 100644 --- a/internal/notification/types/notification.go +++ b/internal/notification/types/notification.go @@ -3,8 +3,10 @@ package types import ( "context" "html" + "strings" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/notification/channels/email" @@ -40,13 +42,17 @@ func SendEmail( triggeringEvent eventstore.Event, ) Notify { return func( - url string, + urlTmpl string, args map[string]interface{}, messageType string, allowUnverifiedNotificationChannel bool, ) error { args = mapNotifyUserToArgs(user, args) sanitizeArgsForHTML(args) + url, err := urlFromTemplate(urlTmpl, args) + if err != nil { + return err + } data := GetTemplateData(ctx, translator, args, url, messageType, user.PreferredLanguage.String(), colors) template, err := templates.GetParsedTemplate(mailhtml, data) if err != nil { @@ -82,6 +88,14 @@ func sanitizeArgsForHTML(args map[string]any) { } } +func urlFromTemplate(urlTmpl string, args map[string]interface{}) (string, error) { + var buf strings.Builder + if err := domain.RenderURLTemplate(&buf, urlTmpl, args); err != nil { + return "", err + } + return buf.String(), nil +} + func SendSMS( ctx context.Context, channels ChannelChains, @@ -92,12 +106,16 @@ func SendSMS( generatorInfo *senders.CodeGeneratorInfo, ) Notify { return func( - url string, + urlTmpl string, args map[string]interface{}, messageType string, allowUnverifiedNotificationChannel bool, ) error { args = mapNotifyUserToArgs(user, args) + url, err := urlFromTemplate(urlTmpl, args) + if err != nil { + return err + } data := GetTemplateData(ctx, translator, args, url, messageType, user.PreferredLanguage.String(), colors) return generateSms( ctx, diff --git a/internal/notification/types/otp.go b/internal/notification/types/otp.go deleted file mode 100644 index 3242b2da3d..0000000000 --- a/internal/notification/types/otp.go +++ /dev/null @@ -1,29 +0,0 @@ -package types - -import ( - "context" - "time" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/domain" -) - -func (notify Notify) SendOTPSMSCode(ctx context.Context, code string, expiry time.Duration) error { - args := otpArgs(ctx, code, expiry) - return notify("", args, domain.VerifySMSOTPMessageType, false) -} - -func (notify Notify) SendOTPEmailCode(ctx context.Context, url, code string, expiry time.Duration) error { - args := otpArgs(ctx, code, expiry) - return notify(url, args, domain.VerifyEmailOTPMessageType, false) -} - -func otpArgs(ctx context.Context, code string, expiry time.Duration) map[string]interface{} { - domainCtx := http_utils.DomainContext(ctx) - args := make(map[string]interface{}) - args["OTP"] = code - args["Origin"] = domainCtx.Origin() - args["Domain"] = domainCtx.RequestedDomain() - args["Expiry"] = expiry - return args -} diff --git a/internal/notification/types/password_change.go b/internal/notification/types/password_change.go deleted file mode 100644 index 8536ac4c04..0000000000 --- a/internal/notification/types/password_change.go +++ /dev/null @@ -1,16 +0,0 @@ -package types - -import ( - "context" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/api/ui/console" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" -) - -func (notify Notify) SendPasswordChange(ctx context.Context, user *query.NotifyUser) error { - url := console.LoginHintLink(http_utils.DomainContext(ctx).Origin(), user.PreferredLoginName) - args := make(map[string]interface{}) - return notify(url, args, domain.PasswordChangeMessageType, true) -} diff --git a/internal/notification/types/password_code.go b/internal/notification/types/password_code.go deleted file mode 100644 index 40ffee3e6d..0000000000 --- a/internal/notification/types/password_code.go +++ /dev/null @@ -1,27 +0,0 @@ -package types - -import ( - "context" - "strings" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" -) - -func (notify Notify) SendPasswordCode(ctx context.Context, user *query.NotifyUser, code, urlTmpl, authRequestID string) error { - var url string - if urlTmpl == "" { - url = login.InitPasswordLink(http_utils.DomainContext(ctx).Origin(), user.ID, code, user.ResourceOwner, authRequestID) - } else { - var buf strings.Builder - if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { - return err - } - url = buf.String() - } - args := make(map[string]interface{}) - args["Code"] = code - return notify(url, args, domain.PasswordResetMessageType, true) -} diff --git a/internal/notification/types/passwordless_registration_link.go b/internal/notification/types/passwordless_registration_link.go deleted file mode 100644 index 64af1a9797..0000000000 --- a/internal/notification/types/passwordless_registration_link.go +++ /dev/null @@ -1,25 +0,0 @@ -package types - -import ( - "context" - "strings" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" -) - -func (notify Notify) SendPasswordlessRegistrationLink(ctx context.Context, user *query.NotifyUser, code, codeID, urlTmpl string) error { - var url string - if urlTmpl == "" { - url = domain.PasswordlessInitCodeLink(http_utils.DomainContext(ctx).Origin()+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code) - } else { - var buf strings.Builder - if err := domain.RenderPasskeyURLTemplate(&buf, urlTmpl, user.ID, user.ResourceOwner, codeID, code); err != nil { - return err - } - url = buf.String() - } - return notify(url, nil, domain.PasswordlessRegistrationMessageType, true) -} diff --git a/internal/notification/types/passwordless_registration_link_test.go b/internal/notification/types/passwordless_registration_link_test.go deleted file mode 100644 index 0a04b7a0fe..0000000000 --- a/internal/notification/types/passwordless_registration_link_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package types - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" -) - -func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { - type args struct { - user *query.NotifyUser - origin *http_utils.DomainCtx - code string - codeID string - urlTmpl string - } - tests := []struct { - name string - args args - want *notifyResult - wantErr error - }{ - { - name: "default URL", - args: args{ - user: &query.NotifyUser{ - ID: "user1", - ResourceOwner: "org1", - }, - origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, - code: "123", - codeID: "456", - urlTmpl: "", - }, - want: ¬ifyResult{ - url: "https://example.com/ui/login/login/passwordless/init?userID=user1&orgID=org1&codeID=456&code=123", - messageType: domain.PasswordlessRegistrationMessageType, - allowUnverifiedNotificationChannel: true, - }, - }, - { - name: "template error", - args: args{ - user: &query.NotifyUser{ - ID: "user1", - ResourceOwner: "org1", - }, - origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, - code: "123", - codeID: "456", - urlTmpl: "{{", - }, - want: ¬ifyResult{}, - wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"), - }, - { - name: "template success", - args: args{ - user: &query.NotifyUser{ - ID: "user1", - ResourceOwner: "org1", - }, - origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, - code: "123", - codeID: "456", - urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}", - }, - want: ¬ifyResult{ - url: "https://example.com/passkey/register?userID=user1&orgID=org1&codeID=456&code=123", - messageType: domain.PasswordlessRegistrationMessageType, - allowUnverifiedNotificationChannel: true, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, notify := mockNotify() - err := notify.SendPasswordlessRegistrationLink(http_utils.WithDomainContext(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.codeID, tt.args.urlTmpl) - require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/internal/notification/types/phone_verification_code.go b/internal/notification/types/phone_verification_code.go deleted file mode 100644 index 461b85749c..0000000000 --- a/internal/notification/types/phone_verification_code.go +++ /dev/null @@ -1,15 +0,0 @@ -package types - -import ( - "context" - - http_util "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/domain" -) - -func (notify Notify) SendPhoneVerificationCode(ctx context.Context, code string) error { - args := make(map[string]interface{}) - args["Code"] = code - args["Domain"] = http_util.DomainContext(ctx).RequestedDomain() - return notify("", args, domain.VerifyPhoneMessageType, true) -} diff --git a/internal/notification/types/types_test.go b/internal/notification/types/types_test.go deleted file mode 100644 index 1b5066d195..0000000000 --- a/internal/notification/types/types_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package types - -type notifyResult struct { - url string - args map[string]interface{} - messageType string - allowUnverifiedNotificationChannel bool -} - -// mockNotify returns a notifyResult and Notify function for easy mocking. -// The notifyResult will only be populated after Notify is called. -func mockNotify() (*notifyResult, Notify) { - dst := new(notifyResult) - return dst, func(url string, args map[string]interface{}, messageType string, allowUnverifiedNotificationChannel bool) error { - *dst = notifyResult{ - url: url, - args: args, - messageType: messageType, - allowUnverifiedNotificationChannel: allowUnverifiedNotificationChannel, - } - return nil - } -} diff --git a/internal/notification/types/user_email.go b/internal/notification/types/user_email.go index 210ca14cf8..985fe81391 100644 --- a/internal/notification/types/user_email.go +++ b/internal/notification/types/user_email.go @@ -74,6 +74,8 @@ func mapNotifyUserToArgs(user *query.NotifyUser, args map[string]interface{}) ma if args == nil { args = make(map[string]interface{}) } + args["UserID"] = user.ID + args["OrgID"] = user.ResourceOwner args["UserName"] = user.Username args["FirstName"] = user.FirstName args["LastName"] = user.LastName @@ -84,6 +86,7 @@ func mapNotifyUserToArgs(user *query.NotifyUser, args map[string]interface{}) ma args["LastPhone"] = user.LastPhone args["VerifiedPhone"] = user.VerifiedPhone args["PreferredLoginName"] = user.PreferredLoginName + args["LoginName"] = user.PreferredLoginName // some endpoint promoted LoginName instead of PreferredLoginName args["LoginNames"] = user.LoginNames args["ChangeDate"] = user.ChangeDate args["CreationDate"] = user.CreationDate diff --git a/internal/repository/notification/aggregate.go b/internal/repository/notification/aggregate.go new file mode 100644 index 0000000000..8370337d40 --- /dev/null +++ b/internal/repository/notification/aggregate.go @@ -0,0 +1,25 @@ +package notification + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + AggregateType = "notification" + AggregateVersion = "v1" +) + +type Aggregate struct { + eventstore.Aggregate +} + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Type: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/notification/eventstore.go b/internal/repository/notification/eventstore.go new file mode 100644 index 0000000000..3ef1c9c7db --- /dev/null +++ b/internal/repository/notification/eventstore.go @@ -0,0 +1,12 @@ +package notification + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, RequestedType, eventstore.GenericEventMapper[RequestedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SentType, eventstore.GenericEventMapper[SentEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, RetryRequestedType, eventstore.GenericEventMapper[RetryRequestedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, CanceledType, eventstore.GenericEventMapper[CanceledEvent]) +} diff --git a/internal/repository/notification/notification.go b/internal/repository/notification/notification.go new file mode 100644 index 0000000000..cf7090525f --- /dev/null +++ b/internal/repository/notification/notification.go @@ -0,0 +1,244 @@ +package notification + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/query" +) + +const ( + notificationEventPrefix = "notification." + RequestedType = notificationEventPrefix + "requested" + RetryRequestedType = notificationEventPrefix + "retry.requested" + SentType = notificationEventPrefix + "sent" + CanceledType = notificationEventPrefix + "canceled" +) + +type Request struct { + UserID string `json:"userID"` + UserResourceOwner string `json:"userResourceOwner"` + AggregateID string `json:"notificationAggregateID"` + AggregateResourceOwner string `json:"notificationAggregateResourceOwner"` + TriggeredAtOrigin string `json:"triggeredAtOrigin"` + EventType eventstore.EventType `json:"eventType"` + MessageType string `json:"messageType"` + NotificationType domain.NotificationType `json:"notificationType"` + URLTemplate string `json:"urlTemplate,omitempty"` + CodeExpiry time.Duration `json:"codeExpiry,omitempty"` + Code *crypto.CryptoValue `json:"code,omitempty"` + UnverifiedNotificationChannel bool `json:"unverifiedNotificationChannel,omitempty"` + IsOTP bool `json:"isOTP,omitempty"` + RequiresPreviousDomain bool `json:"RequiresPreviousDomain,omitempty"` + Args *domain.NotificationArguments `json:"args,omitempty"` +} + +func (e *Request) NotificationAggregateID() string { + if e.AggregateID == "" { + return e.UserID + } + return e.AggregateID +} + +func (e *Request) NotificationAggregateResourceOwner() string { + if e.AggregateResourceOwner == "" { + return e.UserResourceOwner + } + return e.AggregateResourceOwner +} + +type RequestedEvent struct { + eventstore.BaseEvent `json:"-"` + + Request `json:"request"` +} + +func (e *RequestedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + +func (e *RequestedEvent) Payload() interface{} { + return e +} + +func (e *RequestedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *RequestedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} + +func NewRequestedEvent(ctx context.Context, + aggregate *eventstore.Aggregate, + userID, + userResourceOwner, + aggregateID, + aggregateResourceOwner, + triggerOrigin, + urlTemplate string, + code *crypto.CryptoValue, + codeExpiry time.Duration, + eventType eventstore.EventType, + notificationType domain.NotificationType, + messageType string, + unverifiedNotificationChannel, + isOTP, + requiresPreviousDomain bool, + args *domain.NotificationArguments, +) *RequestedEvent { + return &RequestedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + RequestedType, + ), + Request: Request{ + UserID: userID, + UserResourceOwner: userResourceOwner, + AggregateID: aggregateID, + AggregateResourceOwner: aggregateResourceOwner, + TriggeredAtOrigin: triggerOrigin, + EventType: eventType, + MessageType: messageType, + NotificationType: notificationType, + URLTemplate: urlTemplate, + CodeExpiry: codeExpiry, + Code: code, + UnverifiedNotificationChannel: unverifiedNotificationChannel, + IsOTP: isOTP, + RequiresPreviousDomain: requiresPreviousDomain, + Args: args, + }, + } +} + +type SentEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *SentEvent) Payload() interface{} { + return e +} + +func (e *SentEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *SentEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} + +func NewSentEvent(ctx context.Context, + aggregate *eventstore.Aggregate, +) *SentEvent { + return &SentEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SentType, + ), + } +} + +type CanceledEvent struct { + eventstore.BaseEvent `json:"-"` + + Error string `json:"error"` +} + +func (e *CanceledEvent) Payload() interface{} { + return e +} + +func (e *CanceledEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *CanceledEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} + +func NewCanceledEvent(ctx context.Context, aggregate *eventstore.Aggregate, errorMessage string) *CanceledEvent { + return &CanceledEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + CanceledType, + ), + Error: errorMessage, + } +} + +type RetryRequestedEvent struct { + eventstore.BaseEvent `json:"-"` + + Request `json:"request"` + Error string `json:"error"` + NotifyUser *query.NotifyUser `json:"notifyUser"` + BackOff time.Duration `json:"backOff"` +} + +func (e *RetryRequestedEvent) Payload() interface{} { + return e +} + +func (e *RetryRequestedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *RetryRequestedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} + +func NewRetryRequestedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + userID, + userResourceOwner, + aggregateID, + aggregateResourceOwner, + triggerOrigin, + urlTemplate string, + code *crypto.CryptoValue, + codeExpiry time.Duration, + eventType eventstore.EventType, + notificationType domain.NotificationType, + messageType string, + unverifiedNotificationChannel, + isOTP bool, + args *domain.NotificationArguments, + notifyUser *query.NotifyUser, + backoff time.Duration, + errorMessage string, +) *RetryRequestedEvent { + return &RetryRequestedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + RetryRequestedType, + ), + Request: Request{ + UserID: userID, + UserResourceOwner: userResourceOwner, + AggregateID: aggregateID, + AggregateResourceOwner: aggregateResourceOwner, + TriggeredAtOrigin: triggerOrigin, + EventType: eventType, + MessageType: messageType, + NotificationType: notificationType, + URLTemplate: urlTemplate, + CodeExpiry: codeExpiry, + Code: code, + UnverifiedNotificationChannel: unverifiedNotificationChannel, + IsOTP: isOTP, + Args: args, + }, + NotifyUser: notifyUser, + BackOff: backoff, + Error: errorMessage, + } +}