diff --git a/build/Dockerfile b/build/Dockerfile index 4e984fe8e6..769f04023e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -27,6 +27,7 @@ COPY --from=artifact /etc/ssl/certs /etc/ssl/certs COPY --from=artifact /app/zitadel /app/zitadel HEALTHCHECK NONE +EXPOSE 8080 USER zitadel -ENTRYPOINT ["/app/zitadel"] \ No newline at end of file +ENTRYPOINT ["/app/zitadel"] diff --git a/cmd/setup/52.go b/cmd/setup/52.go index 581a7acbab..5b86ba1bad 100644 --- a/cmd/setup/52.go +++ b/cmd/setup/52.go @@ -2,36 +2,26 @@ package setup import ( "context" - "embed" - "fmt" - - "github.com/zitadel/logging" + _ "embed" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" ) -type InitPermittedOrgsFunction52 struct { +var ( + //go:embed 52.sql + renameTableIfNotExisting string +) + +type IDPTemplate6LDAP2 struct { dbClient *database.DB } -//go:embed 52/*.sql -var permittedOrgsFunction52 embed.FS - -func (mig *InitPermittedOrgsFunction52) Execute(ctx context.Context, _ eventstore.Event) error { - statements, err := readStatements(permittedOrgsFunction52, "52") - if err != nil { - return err - } - for _, stmt := range statements { - logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") - if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil { - return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) - } - } - return nil +func (mig *IDPTemplate6LDAP2) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, renameTableIfNotExisting) + return err } -func (*InitPermittedOrgsFunction52) String() string { - return "52_init_permitted_orgs_function" +func (mig *IDPTemplate6LDAP2) String() string { + return "52_idp_templates6_ldap2" } diff --git a/cmd/setup/52.sql b/cmd/setup/52.sql new file mode 100644 index 0000000000..1414b6386c --- /dev/null +++ b/cmd/setup/52.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS projections.idp_templates6_ldap3 RENAME COLUMN rootCA TO root_ca; +ALTER TABLE IF EXISTS projections.idp_templates6_ldap3 RENAME TO idp_templates6_ldap2; diff --git a/cmd/setup/53.go b/cmd/setup/53.go new file mode 100644 index 0000000000..83a7b1c0e2 --- /dev/null +++ b/cmd/setup/53.go @@ -0,0 +1,37 @@ +package setup + +import ( + "context" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +type InitPermittedOrgsFunction53 struct { + dbClient *database.DB +} + +//go:embed 53/*.sql +var permittedOrgsFunction53 embed.FS + +func (mig *InitPermittedOrgsFunction53) Execute(ctx context.Context, _ eventstore.Event) error { + statements, err := readStatements(permittedOrgsFunction53, "53") + if err != nil { + return err + } + for _, stmt := range statements { + logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") + if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil { + return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) + } + } + return nil +} + +func (*InitPermittedOrgsFunction53) String() string { + return "53_init_permitted_orgs_function" +} diff --git a/cmd/setup/52/01-get-permissions-from-JSON.sql b/cmd/setup/53/01-get-permissions-from-JSON.sql similarity index 100% rename from cmd/setup/52/01-get-permissions-from-JSON.sql rename to cmd/setup/53/01-get-permissions-from-JSON.sql diff --git a/cmd/setup/52/02-permitted_orgs_function.sql b/cmd/setup/53/02-permitted_orgs_function.sql similarity index 100% rename from cmd/setup/52/02-permitted_orgs_function.sql rename to cmd/setup/53/02-permitted_orgs_function.sql diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 9754494f9c..da99743516 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -27,6 +27,7 @@ import ( "github.com/zitadel/zitadel/internal/notification/handlers" "github.com/zitadel/zitadel/internal/query/projection" static_config "github.com/zitadel/zitadel/internal/static/config" + metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config" ) type Config struct { @@ -40,6 +41,7 @@ type Config struct { ExternalPort uint16 ExternalSecure bool Log *logging.Config + Metrics metrics.Config EncryptionKeys *encryption.EncryptionKeyConfig DefaultInstance command.InstanceSetup Machine *id.Config @@ -88,6 +90,9 @@ func MustNewConfig(v *viper.Viper) *Config { err = config.Log.SetLogger() logging.OnError(err).Fatal("unable to set logger") + err = config.Metrics.NewMeter() + logging.OnError(err).Fatal("unable to set meter") + id.Configure(config.Machine) // Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API. @@ -143,7 +148,8 @@ type Steps struct { s49InitPermittedOrgsFunction *InitPermittedOrgsFunction s50IDPTemplate6UsePKCE *IDPTemplate6UsePKCE s51IDPTemplate6RootCA *IDPTemplate6RootCA - s52InitPermittedOrgsFunction *InitPermittedOrgsFunction52 + s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2 + s53InitPermittedOrgsFunction *InitPermittedOrgsFunction52 } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 1a6cafbc64..676a9b594a 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -178,7 +178,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s49InitPermittedOrgsFunction = &InitPermittedOrgsFunction{eventstoreClient: dbClient} steps.s50IDPTemplate6UsePKCE = &IDPTemplate6UsePKCE{dbClient: dbClient} steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient} - steps.s52InitPermittedOrgsFunction = &InitPermittedOrgsFunction52{dbClient: dbClient} + steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient} + steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction52{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -219,7 +220,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s49InitPermittedOrgsFunction, steps.s50IDPTemplate6UsePKCE, steps.s51IDPTemplate6RootCA, - steps.s52InitPermittedOrgsFunction, + steps.s52IDPTemplate6LDAP2, + steps.s53InitPermittedOrgsFunction, } { mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") } diff --git a/console/yarn.lock b/console/yarn.lock index fa9085cbbd..d905cfe289 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -3464,7 +3464,7 @@ "@zitadel/proto" "1.0.4" jose "^5.3.0" -"@zitadel/proto@1.0.4", "@zitadel/proto@^1.0.4": +"@zitadel/proto@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.4.tgz#e2fe9895f2960643c3619191255aa2f4913ad873" integrity sha512-s13ZMhuOTe0b+geV+JgJud+kpYdq7TgkuCe7RIY+q4Xs5KC0FHMKfvbAk/jpFbD+TSQHiwo/TBNZlGHdwUR9Ig== diff --git a/docs/yarn.lock b/docs/yarn.lock index fbd899cc3b..96d1552d7e 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -8156,9 +8156,9 @@ mz@^2.7.0: thenify-all "^1.0.0" nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== negotiator@0.6.3: version "0.6.3" diff --git a/e2e/yarn.lock b/e2e/yarn.lock index 17bb06236d..53932921e9 100644 --- a/e2e/yarn.lock +++ b/e2e/yarn.lock @@ -181,9 +181,9 @@ aws4@^1.8.0: integrity sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA== axios@^1.6.1: - version "1.7.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" - integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== + version "1.8.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" + integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" diff --git a/go.mod b/go.mod index 191435e21f..99df9ad86f 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,7 @@ require ( github.com/zitadel/logging v0.6.2 github.com/zitadel/oidc/v3 v3.36.1 github.com/zitadel/passwap v0.7.0 - github.com/zitadel/saml v0.3.4 + github.com/zitadel/saml v0.3.5 github.com/zitadel/schema v1.3.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 diff --git a/go.sum b/go.sum index dab9587cd7..cb8f0cf4bd 100644 --- a/go.sum +++ b/go.sum @@ -809,8 +809,8 @@ github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs= github.com/zitadel/passwap v0.7.0 h1:TQTr9TV75PLATGICor1g5hZDRNHRvB9t0Hn4XkiR7xQ= github.com/zitadel/passwap v0.7.0/go.mod h1:/NakQNYahdU+YFEitVD6mlm8BLfkiIT+IM5wgClRoAY= -github.com/zitadel/saml v0.3.4 h1:L2pybnx2Hs+kqebZmUbnZUd9L/CY2sNw5psMWw2D/6Q= -github.com/zitadel/saml v0.3.4/go.mod h1:M0losAULJpLtAmXrYqBnf375ia2rMgJ75b1mpaU/GlA= +github.com/zitadel/saml v0.3.5 h1:L1RKWS5y66cGepVxUGjx/WSBOtrtSpRA/J3nn5BJLOY= +github.com/zitadel/saml v0.3.5/go.mod h1:ybs3e4tIWdYgSYBpuCsvf3T4FNDfbXYM+GPv5vIpHYk= github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index 73fc995be2..8612d11558 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -10,7 +10,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/api/http" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -158,7 +158,11 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str return nil, err } authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} - ctx = op.ContextWithIssuer(ctx, http.DomainContext(ctx).Origin()) + issuer := authReq.Issuer + if issuer == "" { + issuer = http_utils.DomainContext(ctx).Origin() + } + ctx = op.ContextWithIssuer(ctx, issuer) var callback string if aar.ResponseType == domain.OIDCResponseTypeCode { callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) diff --git a/internal/api/grpc/saml/v2/saml.go b/internal/api/grpc/saml/v2/saml.go index 866846dfd7..43eae5feb1 100644 --- a/internal/api/grpc/saml/v2/saml.go +++ b/internal/api/grpc/saml/v2/saml.go @@ -4,9 +4,11 @@ import ( "context" "github.com/zitadel/logging" + "github.com/zitadel/saml/pkg/provider" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/saml" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -76,6 +78,11 @@ func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID str return nil, err } authReq := &saml.AuthRequestV2{CurrentSAMLRequest: aar} + responseIssuer := authReq.ResponseIssuer + if responseIssuer == "" { + responseIssuer = http_utils.DomainContext(ctx).Origin() + } + ctx = provider.ContextWithIssuer(ctx, responseIssuer) url, body, err := s.idp.CreateResponse(ctx, authReq) if err != nil { return nil, err diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index d433603cd8..a113392df8 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -111,6 +111,7 @@ func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc. Prompt: PromptToBusiness(req.Prompt), UILocales: UILocalesToBusiness(req.UILocales), MaxAge: MaxAgeToBusiness(req.MaxAge), + Issuer: o.contextToIssuer(ctx), } if req.LoginHint != "" { authRequest.LoginHint = &req.LoginHint diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 153a13f06e..37a9ba2bce 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -75,6 +75,7 @@ type OPStorage struct { encAlg crypto.EncryptionAlgorithm locker crdb.Locker assetAPIPrefix func(ctx context.Context) string + contextToIssuer func(context.Context) string } // Provider is used to overload certain [op.Provider] methods @@ -119,7 +120,7 @@ func NewServer( if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } - storage := newStorage(config, command, query, repo, encryptionAlg, es, projections) + storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, ContextToIssuer) keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query)) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) @@ -182,9 +183,13 @@ func NewServer( return server, nil } +func ContextToIssuer(ctx context.Context) string { + return http_utils.DomainContext(ctx).Origin() +} + func IssuerFromContext(_ bool) (op.IssuerFromRequest, error) { return func(r *http.Request) string { - return http_utils.DomainContext(r.Context()).Origin() + return ContextToIssuer(r.Context()) }, nil } @@ -220,7 +225,7 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] return opConfig, nil } -func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB) *OPStorage { +func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB, contextToIssuer func(context.Context) string) *OPStorage { return &OPStorage{ repo: repo, command: command, @@ -236,6 +241,7 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, encAlg: encAlg, locker: crdb.NewLocker(db.DB, locksTable, signingKey), assetAPIPrefix: assets.AssetAPI(), + contextToIssuer: contextToIssuer, } } diff --git a/internal/api/saml/auth_request.go b/internal/api/saml/auth_request.go index f31647f705..db0c74a931 100644 --- a/internal/api/saml/auth_request.go +++ b/internal/api/saml/auth_request.go @@ -3,7 +3,6 @@ package saml import ( "context" "encoding/base64" - "net/http" "net/url" "github.com/zitadel/saml/pkg/provider" @@ -34,15 +33,9 @@ func (p *Provider) CreateResponse(ctx context.Context, authReq models.AuthReques AcsUrl: authReq.GetAccessConsumerServiceURL(), RequestID: authReq.GetAuthRequestID(), Audience: authReq.GetIssuer(), + Issuer: p.GetEntityID(ctx), } - issuer := ContextToIssuer(ctx) - req, err := http.NewRequestWithContext(provider.ContextWithIssuer(ctx, issuer), http.MethodGet, issuer, nil) - if err != nil { - return "", "", err - } - resp.Issuer = p.GetEntityID(req) - samlResponse, err := p.AuthCallbackResponse(ctx, authReq, resp) if err != nil { return "", "", err diff --git a/internal/api/saml/provider.go b/internal/api/saml/provider.go index 0b056797d5..428fc35ed9 100644 --- a/internal/api/saml/provider.go +++ b/internal/api/saml/provider.go @@ -60,6 +60,7 @@ func NewProvider( projections, fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID), conf.DefaultLoginURLV2, + ContextToIssuer, ) if err != nil { return nil, err @@ -117,6 +118,7 @@ func newStorage( db *database.DB, defaultLoginURL string, defaultLoginURLV2 string, + contextToIssuer func(context.Context) string, ) (*Storage, error) { return &Storage{ encAlg: encAlg, @@ -128,6 +130,7 @@ func newStorage( query: query, defaultLoginURL: defaultLoginURL, defaultLoginURLv2: defaultLoginURLV2, + contextToIssuer: contextToIssuer, }, nil } diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index 834cc7b392..935e986c72 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -64,6 +64,7 @@ type Storage struct { defaultLoginURL string defaultLoginURLv2 string + contextToIssuer func(context.Context) string } func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) { @@ -137,14 +138,15 @@ func (p *Storage) createAuthRequestLoginClient(ctx context.Context, req *samlp.A ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() samlRequest := &command.SAMLRequest{ - ApplicationID: applicationID, - ACSURL: acsUrl, - RelayState: relayState, - RequestID: req.Id, - Binding: protocolBinding, - Issuer: req.Issuer.Text, - Destination: req.Destination, - LoginClient: loginClient, + ApplicationID: applicationID, + ACSURL: acsUrl, + RelayState: relayState, + RequestID: req.Id, + Binding: protocolBinding, + Issuer: req.Issuer.Text, + Destination: req.Destination, + LoginClient: loginClient, + ResponseIssuer: p.contextToIssuer(ctx), } aar, err := p.command.AddSAMLRequest(ctx, samlRequest) diff --git a/internal/command/auth_request.go b/internal/command/auth_request.go index 340155d11b..d60012637a 100644 --- a/internal/command/auth_request.go +++ b/internal/command/auth_request.go @@ -29,6 +29,7 @@ type AuthRequest struct { LoginHint *string HintUserID *string NeedRefreshToken bool + Issuer string } type CurrentAuthRequest struct { @@ -73,6 +74,7 @@ func (c *Commands) AddAuthRequest(ctx context.Context, authRequest *AuthRequest) authRequest.LoginHint, authRequest.HintUserID, authRequest.NeedRefreshToken, + authRequest.Issuer, )) if err != nil { return nil, err @@ -180,6 +182,7 @@ func authRequestWriteModelToCurrentAuthRequest(writeModel *AuthRequestWriteModel MaxAge: writeModel.MaxAge, LoginHint: writeModel.LoginHint, HintUserID: writeModel.HintUserID, + Issuer: writeModel.Issuer, }, SessionID: writeModel.SessionID, UserID: writeModel.UserID, diff --git a/internal/command/auth_request_model.go b/internal/command/auth_request_model.go index a6766d1979..0e8d88fd13 100644 --- a/internal/command/auth_request_model.go +++ b/internal/command/auth_request_model.go @@ -36,6 +36,7 @@ type AuthRequestWriteModel struct { AuthMethods []domain.UserAuthMethodType AuthRequestState domain.AuthRequestState NeedRefreshToken bool + Issuer string } func NewAuthRequestWriteModel(ctx context.Context, id string) *AuthRequestWriteModel { @@ -68,6 +69,7 @@ func (m *AuthRequestWriteModel) Reduce() error { m.HintUserID = e.HintUserID m.AuthRequestState = domain.AuthRequestStateAdded m.NeedRefreshToken = e.NeedRefreshToken + m.Issuer = e.Issuer case *authrequest.SessionLinkedEvent: m.SessionID = e.SessionID m.UserID = e.UserID diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index 590e4086f4..c0b5f630f7 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -62,6 +62,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { nil, nil, false, + "issuer", ), ), ), @@ -101,6 +102,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), false, + "issuer", ), ), ), @@ -127,6 +129,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { MaxAge: gu.Ptr(time.Duration(0)), LoginHint: gu.Ptr("loginHint"), HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", }, }, &CurrentAuthRequest{ @@ -150,6 +153,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { MaxAge: gu.Ptr(time.Duration(0)), LoginHint: gu.Ptr("loginHint"), HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", }, }, nil, @@ -234,6 +238,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), eventFromEventPusher( @@ -276,6 +281,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -317,6 +323,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -356,6 +363,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -418,6 +426,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -469,6 +478,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -527,6 +537,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -557,6 +568,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -616,6 +628,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -646,6 +659,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -706,6 +720,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -736,6 +751,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -797,6 +813,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, SessionID: "sessionID", UserID: "userID", @@ -827,6 +844,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -950,6 +968,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { nil, nil, true, + "issuer", ), ), ), @@ -978,6 +997,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { Audience: []string{"audience"}, ResponseType: domain.OIDCResponseTypeCode, ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", }, }, }, @@ -1050,6 +1070,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), ), @@ -1088,6 +1109,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index af1874a6bb..564c39460b 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -138,6 +138,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -182,6 +183,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -234,6 +236,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -331,6 +334,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -465,6 +469,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -610,6 +615,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), true, + "issuer", ), ), eventFromEventPusher( @@ -748,6 +754,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("loginHint"), gu.Ptr("hintUserID"), false, + "issuer", ), ), eventFromEventPusher( diff --git a/internal/command/saml_request.go b/internal/command/saml_request.go index 17f56101ec..40e0643f0c 100644 --- a/internal/command/saml_request.go +++ b/internal/command/saml_request.go @@ -15,13 +15,14 @@ type SAMLRequest struct { ID string LoginClient string - ApplicationID string - ACSURL string - RelayState string - RequestID string - Binding string - Issuer string - Destination string + ApplicationID string + ACSURL string + RelayState string + RequestID string + Binding string + Issuer string + Destination string + ResponseIssuer string } type CurrentSAMLRequest struct { @@ -56,6 +57,7 @@ func (c *Commands) AddSAMLRequest(ctx context.Context, samlRequest *SAMLRequest) samlRequest.Binding, samlRequest.Issuer, samlRequest.Destination, + samlRequest.ResponseIssuer, )) if err != nil { return nil, err @@ -131,15 +133,16 @@ func (c *Commands) FailSAMLRequest(ctx context.Context, id string, reason domain func samlRequestWriteModelToCurrentSAMLRequest(writeModel *SAMLRequestWriteModel) (_ *CurrentSAMLRequest) { return &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: writeModel.AggregateID, - LoginClient: writeModel.LoginClient, - ApplicationID: writeModel.ApplicationID, - ACSURL: writeModel.ACSURL, - RelayState: writeModel.RelayState, - RequestID: writeModel.RequestID, - Binding: writeModel.Binding, - Issuer: writeModel.Issuer, - Destination: writeModel.Destination, + ID: writeModel.AggregateID, + LoginClient: writeModel.LoginClient, + ApplicationID: writeModel.ApplicationID, + ACSURL: writeModel.ACSURL, + RelayState: writeModel.RelayState, + RequestID: writeModel.RequestID, + Binding: writeModel.Binding, + Issuer: writeModel.Issuer, + Destination: writeModel.Destination, + ResponseIssuer: writeModel.ResponseIssuer, }, SessionID: writeModel.SessionID, UserID: writeModel.UserID, diff --git a/internal/command/saml_request_model.go b/internal/command/saml_request_model.go index 7ba640cbe8..afd5b052c7 100644 --- a/internal/command/saml_request_model.go +++ b/internal/command/saml_request_model.go @@ -15,14 +15,15 @@ type SAMLRequestWriteModel struct { eventstore.WriteModel aggregate *eventstore.Aggregate - LoginClient string - ApplicationID string - ACSURL string - RelayState string - RequestID string - Binding string - Issuer string - Destination string + LoginClient string + ApplicationID string + ACSURL string + RelayState string + RequestID string + Binding string + Issuer string + Destination string + ResponseIssuer string SessionID string UserID string @@ -52,6 +53,7 @@ func (m *SAMLRequestWriteModel) Reduce() error { m.Binding = e.Binding m.Issuer = e.Issuer m.Destination = e.Destination + m.ResponseIssuer = e.ResponseIssuer m.SAMLRequestState = domain.SAMLRequestStateAdded case *samlrequest.SessionLinkedEvent: m.SessionID = e.SessionID diff --git a/internal/command/saml_request_test.go b/internal/command/saml_request_test.go index 761edde8fb..c11c87ec48 100644 --- a/internal/command/saml_request_test.go +++ b/internal/command/saml_request_test.go @@ -54,6 +54,7 @@ func TestCommands_AddSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -82,6 +83,7 @@ func TestCommands_AddSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -90,27 +92,29 @@ func TestCommands_AddSAMLRequest(t *testing.T) { args{ ctx: mockCtx, request: &SAMLRequest{ - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, nil, @@ -187,6 +191,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -222,6 +227,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -255,6 +261,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -286,6 +293,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -340,6 +348,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -383,6 +392,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -431,15 +441,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -462,6 +473,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -511,15 +523,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "loginClient", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -541,6 +554,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -591,15 +605,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "loginClient", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -622,6 +637,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -672,15 +688,16 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, authReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "loginClient", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "loginClient", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, SessionID: "sessionID", UserID: "userID", @@ -703,6 +720,7 @@ func TestCommands_LinkSessionToSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -817,6 +835,7 @@ func TestCommands_FailSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), samlrequest.NewFailedEvent(mockCtx, &samlrequest.NewAggregate("V2_id", "instanceID").Aggregate, @@ -850,6 +869,7 @@ func TestCommands_FailSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -870,15 +890,16 @@ func TestCommands_FailSAMLRequest(t *testing.T) { details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, samlReq: &CurrentSAMLRequest{ SAMLRequest: &SAMLRequest{ - ID: "V2_id", - LoginClient: "login", - ApplicationID: "application", - ACSURL: "acs", - RelayState: "relaystate", - RequestID: "request", - Binding: "binding", - Issuer: "issuer", - Destination: "destination", + ID: "V2_id", + LoginClient: "login", + ApplicationID: "application", + ACSURL: "acs", + RelayState: "relaystate", + RequestID: "request", + Binding: "binding", + Issuer: "issuer", + Destination: "destination", + ResponseIssuer: "responseissuer", }, }, }, diff --git a/internal/command/saml_session_test.go b/internal/command/saml_session_test.go index 12cc0683c5..4781381cc4 100644 --- a/internal/command/saml_session_test.go +++ b/internal/command/saml_session_test.go @@ -99,6 +99,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), ), @@ -129,6 +130,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -167,6 +169,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( @@ -248,6 +251,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { "binding", "issuer", "destination", + "responseissuer", ), ), eventFromEventPusher( diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go index 09d5a63825..43c3e58b3b 100644 --- a/internal/eventstore/handler/v2/handler.go +++ b/internal/eventstore/handler/v2/handler.go @@ -67,6 +67,8 @@ type Handler struct { cacheInvalidations []func(ctx context.Context, aggregates []*eventstore.Aggregate) queryInstances func() ([]string, error) + + metrics *ProjectionMetrics } var _ migration.Migration = (*Handler)(nil) @@ -159,6 +161,8 @@ func NewHandler( aggregates[reducer.Aggregate] = eventTypes } + metrics := NewProjectionMetrics() + handler := &Handler{ projection: projection, client: config.Client, @@ -178,6 +182,7 @@ func NewHandler( } return nil, nil }, + metrics: metrics, } return handler @@ -483,6 +488,8 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add defer cancel() } + start := time.Now() + tx, err := h.client.BeginTx(txCtx, nil) if err != nil { return false, err @@ -502,7 +509,7 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add } return additionalIteration, err } - // stop execution if currentState.eventTimestamp >= config.maxCreatedAt + // stop execution if currentState.position >= config.maxPosition if config.maxPosition != 0 && currentState.position >= config.maxPosition { return false, nil } @@ -518,7 +525,14 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add if err == nil { err = commitErr } + + h.metrics.ProjectionEventsProcessed(ctx, h.ProjectionName(), int64(len(statements)), err == nil) + if err == nil && currentState.aggregateID != "" && len(statements) > 0 { + // Don't update projection timing or latency unless we successfully processed events + h.metrics.ProjectionUpdateTiming(ctx, h.ProjectionName(), float64(time.Since(start).Seconds())) + h.metrics.ProjectionStateLatency(ctx, h.ProjectionName(), time.Since(currentState.eventTimestamp).Seconds()) + h.invalidateCaches(ctx, aggregatesFromStatements(statements)) } }() @@ -540,6 +554,7 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add currentState.aggregateType = statements[lastProcessedIndex].Aggregate.Type currentState.sequence = statements[lastProcessedIndex].Sequence currentState.eventTimestamp = statements[lastProcessedIndex].CreationDate + err = h.setState(tx, currentState) return additionalIteration, err diff --git a/internal/eventstore/handler/v2/metrics.go b/internal/eventstore/handler/v2/metrics.go new file mode 100644 index 0000000000..6876bb3aa4 --- /dev/null +++ b/internal/eventstore/handler/v2/metrics.go @@ -0,0 +1,70 @@ +package handler + +import ( + "context" + + "github.com/zitadel/logging" + "go.opentelemetry.io/otel/attribute" + + "github.com/zitadel/zitadel/internal/telemetry/metrics" +) + +const ( + ProjectionLabel = "projection" + SuccessLabel = "success" + + ProjectionEventsProcessed = "projection_events_processed" + ProjectionHandleTimerMetric = "projection_handle_timer" + ProjectionStateLatencyMetric = "projection_state_latency" +) + +type ProjectionMetrics struct { + provider metrics.Metrics +} + +func NewProjectionMetrics() *ProjectionMetrics { + projectionMetrics := &ProjectionMetrics{provider: metrics.M} + + err := projectionMetrics.provider.RegisterCounter( + ProjectionEventsProcessed, + "Number of events reduced to process projection updates", + ) + logging.OnError(err).Error("failed to register projection events processed counter") + err = projectionMetrics.provider.RegisterHistogram( + ProjectionHandleTimerMetric, + "Time taken to process a projection update", + "s", + []float64{0.005, 0.01, 0.05, 0.1, 1, 5, 10, 30, 60, 120}, + ) + logging.OnError(err).Error("failed to register projection handle timer metric") + err = projectionMetrics.provider.RegisterHistogram( + ProjectionStateLatencyMetric, + "When finishing processing a batch of events, this track the age of the last events seen from current time", + "s", + []float64{0.1, 0.5, 1, 5, 10, 30, 60, 300, 600, 1800}, + ) + logging.OnError(err).Error("failed to register projection state latency metric") + return projectionMetrics +} + +func (m *ProjectionMetrics) ProjectionUpdateTiming(ctx context.Context, projection string, duration float64) { + err := m.provider.AddHistogramMeasurement(ctx, ProjectionHandleTimerMetric, duration, map[string]attribute.Value{ + ProjectionLabel: attribute.StringValue(projection), + }) + logging.OnError(err).Error("failed to add projection trigger timing") +} + +func (m *ProjectionMetrics) ProjectionEventsProcessed(ctx context.Context, projection string, count int64, success bool) { + err := m.provider.AddCount(ctx, ProjectionEventsProcessed, count, map[string]attribute.Value{ + ProjectionLabel: attribute.StringValue(projection), + SuccessLabel: attribute.BoolValue(success), + }) + logging.OnError(err).Error("failed to add projection events processed metric") +} + +func (m *ProjectionMetrics) ProjectionStateLatency(ctx context.Context, projection string, latency float64) { + err := m.provider.AddHistogramMeasurement(ctx, ProjectionStateLatencyMetric, latency, map[string]attribute.Value{ + ProjectionLabel: attribute.StringValue(projection), + }) + logging.OnError(err).Error("failed to add projection state latency metric") +} diff --git a/internal/eventstore/handler/v2/metrics_test.go b/internal/eventstore/handler/v2/metrics_test.go new file mode 100644 index 0000000000..54e7623462 --- /dev/null +++ b/internal/eventstore/handler/v2/metrics_test.go @@ -0,0 +1,132 @@ +package handler + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/telemetry/metrics" +) + +func TestNewProjectionMetrics(t *testing.T) { + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + + metrics := NewProjectionMetrics() + require.NotNil(t, metrics) + assert.NotNil(t, metrics.provider) +} + +func TestProjectionMetrics_ProjectionUpdateTiming(t *testing.T) { + + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + projectionMetrics := NewProjectionMetrics() + + ctx := context.Background() + projection := "test_projection" + duration := 0.5 + + projectionMetrics.ProjectionUpdateTiming(ctx, projection, duration) + + values := mockMetrics.GetHistogramValues(ProjectionHandleTimerMetric) + require.Len(t, values, 1) + assert.Equal(t, duration, values[0]) + + labels := mockMetrics.GetHistogramLabels(ProjectionHandleTimerMetric) + require.Len(t, labels, 1) + assert.Equal(t, projection, labels[0][ProjectionLabel].AsString()) +} + +func TestProjectionMetrics_ProjectionEventsProcessed(t *testing.T) { + + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + projectionMetrics := NewProjectionMetrics() + + ctx := context.Background() + projection := "test_projection" + count := int64(5) + success := true + + projectionMetrics.ProjectionEventsProcessed(ctx, projection, count, success) + + value := mockMetrics.GetCounterValue(ProjectionEventsProcessed) + assert.Equal(t, count, value) + + labels := mockMetrics.GetCounterLabels(ProjectionEventsProcessed) + require.Len(t, labels, 1) + assert.Equal(t, projection, labels[0][ProjectionLabel].AsString()) + assert.Equal(t, success, labels[0][SuccessLabel].AsBool()) +} + +func TestProjectionMetrics_ProjectionStateLatency(t *testing.T) { + + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + projectionMetrics := NewProjectionMetrics() + + ctx := context.Background() + projection := "test_projection" + latency := 10.0 + + projectionMetrics.ProjectionStateLatency(ctx, projection, latency) + + values := mockMetrics.GetHistogramValues(ProjectionStateLatencyMetric) + require.Len(t, values, 1) + assert.Equal(t, latency, values[0]) + + labels := mockMetrics.GetHistogramLabels(ProjectionStateLatencyMetric) + require.Len(t, labels, 1) + assert.Equal(t, projection, labels[0][ProjectionLabel].AsString()) +} + +func TestProjectionMetrics_Integration(t *testing.T) { + + mockMetrics := metrics.NewMockMetrics() + metrics.M = mockMetrics + projectionMetrics := NewProjectionMetrics() + + ctx := context.Background() + projection := "test_projection" + + start := time.Now() + + projectionMetrics.ProjectionEventsProcessed(ctx, projection, 3, true) + projectionMetrics.ProjectionEventsProcessed(ctx, projection, 1, false) + + duration := time.Since(start).Seconds() + projectionMetrics.ProjectionUpdateTiming(ctx, projection, duration) + + latency := 5.0 + projectionMetrics.ProjectionStateLatency(ctx, projection, latency) + + value := mockMetrics.GetCounterValue(ProjectionEventsProcessed) + assert.Equal(t, int64(4), value) + + timingValues := mockMetrics.GetHistogramValues(ProjectionHandleTimerMetric) + require.Len(t, timingValues, 1) + assert.Equal(t, duration, timingValues[0]) + + latencyValues := mockMetrics.GetHistogramValues(ProjectionStateLatencyMetric) + require.Len(t, latencyValues, 1) + assert.Equal(t, latency, latencyValues[0]) + + eventsLabels := mockMetrics.GetCounterLabels(ProjectionEventsProcessed) + require.Len(t, eventsLabels, 2) + assert.Equal(t, projection, eventsLabels[0][ProjectionLabel].AsString()) + assert.Equal(t, true, eventsLabels[0][SuccessLabel].AsBool()) + assert.Equal(t, projection, eventsLabels[1][ProjectionLabel].AsString()) + assert.Equal(t, false, eventsLabels[1][SuccessLabel].AsBool()) + + timingLabels := mockMetrics.GetHistogramLabels(ProjectionHandleTimerMetric) + require.Len(t, timingLabels, 1) + assert.Equal(t, projection, timingLabels[0][ProjectionLabel].AsString()) + + latencyLabels := mockMetrics.GetHistogramLabels(ProjectionStateLatencyMetric) + require.Len(t, latencyLabels, 1) + assert.Equal(t, projection, latencyLabels[0][ProjectionLabel].AsString()) +} diff --git a/internal/repository/authrequest/auth_request.go b/internal/repository/authrequest/auth_request.go index 99f034333b..75624e3a21 100644 --- a/internal/repository/authrequest/auth_request.go +++ b/internal/repository/authrequest/auth_request.go @@ -38,6 +38,7 @@ type AddedEvent struct { LoginHint *string `json:"login_hint,omitempty"` HintUserID *string `json:"hint_user_id,omitempty"` NeedRefreshToken bool `json:"need_refresh_token,omitempty"` + Issuer string `json:"issuer,omitempty"` } func (e *AddedEvent) Payload() interface{} { @@ -66,6 +67,7 @@ func NewAddedEvent(ctx context.Context, loginHint, hintUserID *string, needRefreshToken bool, + issuer string, ) *AddedEvent { return &AddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -89,6 +91,7 @@ func NewAddedEvent(ctx context.Context, LoginHint: loginHint, HintUserID: hintUserID, NeedRefreshToken: needRefreshToken, + Issuer: issuer, } } diff --git a/internal/repository/samlrequest/saml_request.go b/internal/repository/samlrequest/saml_request.go index b3ecdd753e..aca8da99fe 100644 --- a/internal/repository/samlrequest/saml_request.go +++ b/internal/repository/samlrequest/saml_request.go @@ -19,14 +19,15 @@ const ( type AddedEvent struct { *eventstore.BaseEvent `json:"-"` - LoginClient string `json:"login_client,omitempty"` - ApplicationID string `json:"application_id,omitempty"` - ACSURL string `json:"acs_url,omitempty"` - RelayState string `json:"relay_state,omitempty"` - RequestID string `json:"request_id,omitempty"` - Binding string `json:"binding,omitempty"` - Issuer string `json:"issuer,omitempty"` - Destination string `json:"destination,omitempty"` + LoginClient string `json:"login_client,omitempty"` + ApplicationID string `json:"application_id,omitempty"` + ACSURL string `json:"acs_url,omitempty"` + RelayState string `json:"relay_state,omitempty"` + RequestID string `json:"request_id,omitempty"` + Binding string `json:"binding,omitempty"` + Issuer string `json:"issuer,omitempty"` + Destination string `json:"destination,omitempty"` + ResponseIssuer string `json:"response_issuer,omitempty"` } func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { @@ -51,6 +52,7 @@ func NewAddedEvent(ctx context.Context, binding string, issuer string, destination string, + responseIssuer string, ) *AddedEvent { return &AddedEvent{ BaseEvent: eventstore.NewBaseEventForPush( @@ -58,14 +60,15 @@ func NewAddedEvent(ctx context.Context, aggregate, AddedType, ), - LoginClient: loginClient, - ApplicationID: applicationID, - ACSURL: acsURL, - RelayState: relayState, - RequestID: requestID, - Binding: binding, - Issuer: issuer, - Destination: destination, + LoginClient: loginClient, + ApplicationID: applicationID, + ACSURL: acsURL, + RelayState: relayState, + RequestID: requestID, + Binding: binding, + Issuer: issuer, + Destination: destination, + ResponseIssuer: responseIssuer, } } diff --git a/internal/telemetry/metrics/config/config.go b/internal/telemetry/metrics/config/config.go index e9bcbe45c2..9e9ebec52b 100644 --- a/internal/telemetry/metrics/config/config.go +++ b/internal/telemetry/metrics/config/config.go @@ -1,6 +1,7 @@ package config import ( + "github.com/zitadel/zitadel/internal/telemetry/metrics" "github.com/zitadel/zitadel/internal/telemetry/metrics/otel" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -12,11 +13,16 @@ type Config struct { var meter = map[string]func(map[string]interface{}) error{ "otel": otel.NewTracerFromConfig, - "none": NoMetrics, - "": NoMetrics, + "none": registerNoopMetrics, + "": registerNoopMetrics, } func (c *Config) NewMeter() error { + // When using start-from-init or start-from-setup the metric provider + // was already set in the setup phase and the start phase must not overwrite it. + if metrics.M != nil { + return nil + } t, ok := meter[c.Type] if !ok { return zerrors.ThrowInternalf(nil, "METER-Dfqsx", "config type %s not supported", c.Type) @@ -25,6 +31,7 @@ func (c *Config) NewMeter() error { return t(c.Config) } -func NoMetrics(_ map[string]interface{}) error { +func registerNoopMetrics(rawConfig map[string]interface{}) (err error) { + metrics.M = &metrics.NoopMetrics{} return nil } diff --git a/internal/telemetry/metrics/metrics.go b/internal/telemetry/metrics/metrics.go index 503ebc22de..b25dc619c0 100644 --- a/internal/telemetry/metrics/metrics.go +++ b/internal/telemetry/metrics/metrics.go @@ -22,8 +22,10 @@ type Metrics interface { GetMetricsProvider() metric.MeterProvider RegisterCounter(name, description string) error AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error + AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error + RegisterHistogram(name, description, unit string, buckets []float64) error } var M Metrics @@ -56,6 +58,20 @@ func AddCount(ctx context.Context, name string, value int64, labels map[string]a return M.AddCount(ctx, name, value, labels) } +func AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + if M == nil { + return nil + } + return M.AddHistogramMeasurement(ctx, name, value, labels) +} + +func RegisterHistogram(name, description, unit string, buckets []float64) error { + if M == nil { + return nil + } + return M.RegisterHistogram(name, description, unit, buckets) +} + func RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { if M == nil { return nil diff --git a/internal/telemetry/metrics/mock.go b/internal/telemetry/metrics/mock.go new file mode 100644 index 0000000000..b28a6f5d40 --- /dev/null +++ b/internal/telemetry/metrics/mock.go @@ -0,0 +1,95 @@ +package metrics + +import ( + "context" + "net/http" + "sync" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +// MockMetrics implements the metrics.Metrics interface for testing +type MockMetrics struct { + mu sync.RWMutex + histogramValues map[string][]float64 + counterValues map[string]int64 + histogramLabels map[string][]map[string]attribute.Value + counterLabels map[string][]map[string]attribute.Value +} + +var _ Metrics = new(MockMetrics) + +// NewMockMetrics creates a new Metrics instance for testing +func NewMockMetrics() *MockMetrics { + return &MockMetrics{ + histogramValues: make(map[string][]float64), + counterValues: make(map[string]int64), + histogramLabels: make(map[string][]map[string]attribute.Value), + counterLabels: make(map[string][]map[string]attribute.Value), + } +} + +func (m *MockMetrics) GetExporter() http.Handler { + return nil +} + +func (m *MockMetrics) GetMetricsProvider() metric.MeterProvider { + return nil +} + +func (m *MockMetrics) RegisterCounter(name, description string) error { + return nil +} + +func (m *MockMetrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error { + m.mu.Lock() + defer m.mu.Unlock() + m.counterValues[name] += value + m.counterLabels[name] = append(m.counterLabels[name], labels) + return nil +} + +func (m *MockMetrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + m.mu.Lock() + defer m.mu.Unlock() + m.histogramValues[name] = append(m.histogramValues[name], value) + m.histogramLabels[name] = append(m.histogramLabels[name], labels) + return nil +} + +func (m *MockMetrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { + return nil +} + +func (m *MockMetrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error { + return nil +} + +func (m *MockMetrics) RegisterHistogram(name, description, unit string, buckets []float64) error { + return nil +} + +func (m *MockMetrics) GetHistogramValues(name string) []float64 { + m.mu.RLock() + defer m.mu.RUnlock() + return m.histogramValues[name] +} + +func (m *MockMetrics) GetHistogramLabels(name string) []map[string]attribute.Value { + m.mu.RLock() + defer m.mu.RUnlock() + return m.histogramLabels[name] +} + +func (m *MockMetrics) GetCounterValue(name string) int64 { + m.mu.RLock() + defer m.mu.RUnlock() + return m.counterValues[name] +} + +func (m *MockMetrics) GetCounterLabels(name string) []map[string]attribute.Value { + m.mu.RLock() + defer m.mu.RUnlock() + return m.counterLabels[name] +} diff --git a/internal/telemetry/metrics/noop.go b/internal/telemetry/metrics/noop.go new file mode 100644 index 0000000000..954db1d2b9 --- /dev/null +++ b/internal/telemetry/metrics/noop.go @@ -0,0 +1,45 @@ +package metrics + +import ( + "context" + "net/http" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +type NoopMetrics struct{} + +var _ Metrics = new(NoopMetrics) + +func (n *NoopMetrics) GetExporter() http.Handler { + return nil +} + +func (n *NoopMetrics) GetMetricsProvider() metric.MeterProvider { + return nil +} + +func (n *NoopMetrics) RegisterCounter(name, description string) error { + return nil +} + +func (n *NoopMetrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error { + return nil +} + +func (n *NoopMetrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + return nil +} + +func (n *NoopMetrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { + return nil +} + +func (n *NoopMetrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error { + return nil +} + +func (n *NoopMetrics) RegisterHistogram(name, description, unit string, buckets []float64) error { + return nil +} diff --git a/internal/telemetry/metrics/otel/open_telemetry.go b/internal/telemetry/metrics/otel/open_telemetry.go index 5335e65234..c4509ed5db 100644 --- a/internal/telemetry/metrics/otel/open_telemetry.go +++ b/internal/telemetry/metrics/otel/open_telemetry.go @@ -24,6 +24,7 @@ type Metrics struct { Counters sync.Map UpDownSumObserver sync.Map ValueObservers sync.Map + Histograms sync.Map } func NewMetrics(meterName string) (metrics.Metrics, error) { @@ -84,6 +85,33 @@ func (m *Metrics) AddCount(ctx context.Context, name string, value int64, labels return nil } +func (m *Metrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + histogram, exists := m.Histograms.Load(name) + if !exists { + return zerrors.ThrowNotFound(nil, "METER-5wwb1", "Errors.Metrics.Histogram.NotFound") + } + histogram.(metric.Float64Histogram).Record(ctx, value, MapToRecordOption(labels)...) + return nil +} + +func (m *Metrics) RegisterHistogram(name, description, unit string, buckets []float64) error { + if _, exists := m.Histograms.Load(name); exists { + return nil + } + + histogram, err := m.Meter.Float64Histogram(name, + metric.WithDescription(description), + metric.WithUnit(unit), + metric.WithExplicitBucketBoundaries(buckets...), + ) + if err != nil { + return err + } + + m.Histograms.Store(name, histogram) + return nil +} + func (m *Metrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { if _, exists := m.UpDownSumObserver.Load(name); exists { return nil @@ -113,15 +141,23 @@ func (m *Metrics) RegisterValueObserver(name, description string, callbackFunc m } func MapToAddOption(labels map[string]attribute.Value) []metric.AddOption { + return []metric.AddOption{metric.WithAttributes(labelsToAttributes(labels)...)} +} + +func MapToRecordOption(labels map[string]attribute.Value) []metric.RecordOption { + return []metric.RecordOption{metric.WithAttributes(labelsToAttributes(labels)...)} +} + +func labelsToAttributes(labels map[string]attribute.Value) []attribute.KeyValue { if labels == nil { return nil } - keyValues := make([]attribute.KeyValue, 0, len(labels)) + attributes := make([]attribute.KeyValue, 0, len(labels)) for key, value := range labels { - keyValues = append(keyValues, attribute.KeyValue{ + attributes = append(attributes, attribute.KeyValue{ Key: attribute.Key(key), Value: value, }) } - return []metric.AddOption{metric.WithAttributes(keyValues...)} + return attributes }