diff --git a/cmd/start/start.go b/cmd/start/start.go index 794d629a59..f4be4d33ad 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -217,6 +217,7 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error if err != nil { return fmt.Errorf("cannot start commands: %w", err) } + defer commands.Close(ctx) // wait for background jobs clock := clockpkg.New() actionsExecutionStdoutEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.ExecutionLog]()) diff --git a/go.mod b/go.mod index 815aad0685..6b4b6c530e 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/allegro/bigcache v1.2.1 github.com/benbjohnson/clock v1.3.5 github.com/boombuler/barcode v1.0.1 + github.com/brianvoe/gofakeit/v6 v6.25.0 github.com/cockroachdb/cockroach-go/v2 v2.3.5 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/crewjam/saml v0.4.14 @@ -49,7 +50,6 @@ require ( github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 github.com/nicksnyder/go-i18n/v2 v2.2.2 - github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/rakyll/statik v0.1.7 github.com/rs/cors v1.10.1 @@ -60,7 +60,7 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.5.0 - github.com/zitadel/oidc/v3 v3.4.0 + github.com/zitadel/oidc/v3 v3.5.0 github.com/zitadel/passwap v0.4.0 github.com/zitadel/saml v0.1.2 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 @@ -74,10 +74,10 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.20.0 go.opentelemetry.io/otel/trace v1.21.0 go.uber.org/mock v0.3.0 - golang.org/x/crypto v0.15.0 + golang.org/x/crypto v0.16.0 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 - golang.org/x/net v0.18.0 - golang.org/x/oauth2 v0.14.0 + golang.org/x/net v0.19.0 + golang.org/x/oauth2 v0.15.0 golang.org/x/sync v0.5.0 golang.org/x/text v0.14.0 google.golang.org/api v0.150.0 @@ -107,6 +107,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -201,7 +202,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/sys v0.14.0 + golang.org/x/sys v0.15.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 98ea3f3ce3..7ea8f8f0bf 100644 --- a/go.sum +++ b/go.sum @@ -122,6 +122,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/brianvoe/gofakeit/v6 v6.25.0 h1:ZpFjktOpLZUeF8q223o0rUuXtA+m5qW5srjvVi+JkXk= +github.com/brianvoe/gofakeit/v6 v6.25.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -865,8 +867,8 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= -github.com/zitadel/oidc/v3 v3.4.0 h1:JkbNnrk/7IG+NOBoZp/P0kx6tPcBvnCekSqDTPCOok4= -github.com/zitadel/oidc/v3 v3.4.0/go.mod h1:jUnLnx5ihKlo88cSEduZkKlzeMrjzcWVZ8fTzKBxZKY= +github.com/zitadel/oidc/v3 v3.5.0 h1:z51AN6FPo5UuwYJ1r9nLvHlxpTGYd8QXg5MrtYm/dgM= +github.com/zitadel/oidc/v3 v3.5.0/go.mod h1:R8sF5DPR98QQnOoyySsaNqI4NcF/VFMkf/XoYiBUuXQ= github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0= github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g= github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo= @@ -951,8 +953,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1040,8 +1042,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1051,8 +1053,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1133,8 +1135,9 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/api/grpc/user/converter.go b/internal/api/grpc/user/converter.go index 7b00d2f4cc..eca346bad8 100644 --- a/internal/api/grpc/user/converter.go +++ b/internal/api/grpc/user/converter.go @@ -72,7 +72,7 @@ func MachineToPb(view *query.Machine) *user_pb.Machine { return &user_pb.Machine{ Name: view.Name, Description: view.Description, - HasSecret: view.HasSecret, + HasSecret: view.Secret != nil, AccessTokenType: AccessTokenTypeToPb(view.AccessTokenType), } } diff --git a/internal/api/oidc/access_token.go b/internal/api/oidc/access_token.go index d01badda98..35ab1edea6 100644 --- a/internal/api/oidc/access_token.go +++ b/internal/api/oidc/access_token.go @@ -10,7 +10,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/command" - errz "github.com/zitadel/zitadel/internal/errors" + zerrors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/user/model" ) @@ -55,7 +55,7 @@ func (s *Server) verifyAccessToken(ctx context.Context, tkn string) (*accessToke token, err := s.repo.TokenByIDs(ctx, subject, tokenID) if err != nil { - return nil, errz.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired") + return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired") } return accessTokenV1(tokenID, subject, token), nil } @@ -91,7 +91,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke token.audience = append(token.audience, clientID) projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID) if err != nil { - return errz.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") + return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } roles, err := s.query.SearchProjectRoles(ctx, s.features.TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) if err != nil { diff --git a/internal/api/oidc/auth_request_integration_test.go b/internal/api/oidc/auth_request_integration_test.go index 83762e338a..9f8e77688d 100644 --- a/internal/api/oidc/auth_request_integration_test.go +++ b/internal/api/oidc/auth_request_integration_test.go @@ -17,6 +17,7 @@ import ( http_utils "github.com/zitadel/zitadel/internal/api/http" oidc_api "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/integration" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" ) @@ -500,8 +501,7 @@ func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDT provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) - codeVerifier := "codeVerifier" - return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(codeVerifier)) + return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(integration.CodeVerifier)) } func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) { diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 7035f3db56..6514583564 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -43,32 +43,14 @@ const ( func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Client, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - client, err := o.query.AppByOIDCClientID(ctx, id) + client, err := o.query.GetOIDCClientByID(ctx, id, false) if err != nil { return nil, err } if client.State != domain.AppStateActive { return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sdaGg", "client is not active") } - projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(client.ProjectID) - if err != nil { - return nil, errors.ThrowInternal(err, "OIDC-mPxqP", "Errors.Internal") - } - projectRoles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) - if err != nil { - return nil, err - } - allowedScopes := make([]string, len(projectRoles.ProjectRoles)) - for i, role := range projectRoles.ProjectRoles { - allowedScopes[i] = ScopeProjectRolePrefix + role.Key - } - - accessTokenLifetime, idTokenLifetime, _, _, err := o.getOIDCSettings(ctx) - if err != nil { - return nil, err - } - - return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2, accessTokenLifetime, idTokenLifetime, allowedScopes) + return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2), nil } func (o *OPStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (_ *jose.JSONWebKey, err error) { @@ -235,22 +217,10 @@ func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID }, nil } -func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) { - loginname, err := query.NewUserLoginNamesSearchQuery(clientID) - if err != nil { - return nil, err - } - user, err := o.query.GetUser(ctx, false, loginname) - if err != nil { - return nil, err - } - if _, err := o.command.VerifyMachineSecret(ctx, user.ID, user.ResourceOwner, clientSecret); err != nil { - return nil, err - } - return &clientCredentialsClient{ - id: clientID, - tokenType: accessTokenTypeToOIDC(user.Machine.AccessTokenType), - }, nil +// ClientCredentials method is kept to keep the storage interface implemented. +// However, it should never be called as the VerifyClient method on the Server is overridden. +func (o *OPStorage) ClientCredentials(context.Context, string, string) (op.Client, error) { + return nil, errors.ThrowInternal(nil, "OIDC-Su8So", "Errors.Internal") } // isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin @@ -934,3 +904,67 @@ func userinfoClaims(userInfo *oidc.UserInfo) func(c *actions.FieldConfig) interf return c.Runtime.ToValue(claims) } } + +func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if oidc.GrantType(r.Form.Get("grant_type")) == oidc.GrantTypeClientCredentials { + return s.clientCredentialsAuth(ctx, r.Data.ClientID, r.Data.ClientSecret) + } + + clientID, assertion, err := clientIDFromCredentials(r.Data) + if err != nil { + return nil, err + } + client, err := s.query.GetOIDCClientByID(ctx, clientID, assertion) + if errors.IsNotFound(err) { + return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found") + } + if err != nil { + return nil, err // defaults to server error + } + if client.State != domain.AppStateActive { + return nil, oidc.ErrInvalidClient().WithDescription("client is not active") + } + + switch client.AuthMethodType { + case domain.OIDCAuthMethodTypeBasic, domain.OIDCAuthMethodTypePost: + err = s.verifyClientSecret(ctx, client, r.Data.ClientSecret) + case domain.OIDCAuthMethodTypePrivateKeyJWT: + err = s.verifyClientAssertion(ctx, client, r.Data.ClientAssertion) + case domain.OIDCAuthMethodTypeNone: + } + if err != nil { + return nil, err + } + + return ClientFromBusiness(client, s.defaultLoginURL, s.defaultLoginURLV2), nil +} + +func (s *Server) verifyClientAssertion(ctx context.Context, client *query.OIDCClient, assertion string) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if assertion == "" { + return oidc.ErrInvalidClient().WithDescription("empty client assertion") + } + verifier := op.NewJWTProfileVerifierKeySet(keySetMap(client.PublicKeys), op.IssuerFromContext(ctx), time.Hour, client.ClockSkew) + if _, err := op.VerifyJWTAssertion(ctx, assertion, verifier); err != nil { + return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid assertion") + } + return nil +} + +func (s *Server) verifyClientSecret(ctx context.Context, client *query.OIDCClient, secret string) (err error) { + _, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if secret == "" { + return oidc.ErrInvalidClient().WithDescription("empty client secret") + } + if err = crypto.CompareHash(client.ClientSecret, []byte(secret), s.hashAlg); err != nil { + return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid secret") + } + return nil +} diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index ec208db27c..1a6c5ed7c1 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -1,6 +1,7 @@ package oidc import ( + "slices" "strings" "time" @@ -9,43 +10,40 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" ) type Client struct { - app *query.App - defaultLoginURL string - defaultLoginURLV2 string - defaultAccessTokenLifetime time.Duration - defaultIdTokenLifetime time.Duration - allowedScopes []string + client *query.OIDCClient + defaultLoginURL string + defaultLoginURLV2 string + allowedScopes []string } -func ClientFromBusiness(app *query.App, defaultLoginURL, defaultLoginURLV2 string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration, allowedScopes []string) (op.Client, error) { - if app.OIDCConfig == nil { - return nil, errors.ThrowInvalidArgument(nil, "OIDC-d5bhD", "client is not a proper oidc application") +func ClientFromBusiness(client *query.OIDCClient, defaultLoginURL, defaultLoginURLV2 string) op.Client { + allowedScopes := make([]string, len(client.ProjectRoleKeys)) + for i, roleKey := range client.ProjectRoleKeys { + allowedScopes[i] = ScopeProjectRolePrefix + roleKey } + return &Client{ - app: app, - defaultLoginURL: defaultLoginURL, - defaultLoginURLV2: defaultLoginURLV2, - defaultAccessTokenLifetime: defaultAccessTokenLifetime, - defaultIdTokenLifetime: defaultIdTokenLifetime, - allowedScopes: allowedScopes}, - nil + client: client, + defaultLoginURL: defaultLoginURL, + defaultLoginURLV2: defaultLoginURLV2, + allowedScopes: allowedScopes, + } } func (c *Client) ApplicationType() op.ApplicationType { - return op.ApplicationType(c.app.OIDCConfig.AppType) + return op.ApplicationType(c.client.ApplicationType) } func (c *Client) AuthMethod() oidc.AuthMethod { - return authMethodToOIDC(c.app.OIDCConfig.AuthMethodType) + return authMethodToOIDC(c.client.AuthMethodType) } func (c *Client) GetID() string { - return c.app.OIDCConfig.ClientID + return c.client.ClientID } func (c *Client) LoginURL(id string) string { @@ -56,28 +54,28 @@ func (c *Client) LoginURL(id string) string { } func (c *Client) RedirectURIs() []string { - return c.app.OIDCConfig.RedirectURIs + return c.client.RedirectURIs } func (c *Client) PostLogoutRedirectURIs() []string { - return c.app.OIDCConfig.PostLogoutRedirectURIs + return c.client.PostLogoutRedirectURIs } func (c *Client) ResponseTypes() []oidc.ResponseType { - return responseTypesToOIDC(c.app.OIDCConfig.ResponseTypes) + return responseTypesToOIDC(c.client.ResponseTypes) } func (c *Client) GrantTypes() []oidc.GrantType { - return grantTypesToOIDC(c.app.OIDCConfig.GrantTypes) + return grantTypesToOIDC(c.client.GrantTypes) } func (c *Client) DevMode() bool { - return c.app.OIDCConfig.IsDevMode + return c.client.IsDevMode } func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { - if c.app.OIDCConfig.AssertIDTokenRole { + if c.client.IDTokenRoleAssertion { return scopes } return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix) @@ -86,7 +84,7 @@ func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []strin func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { - if c.app.OIDCConfig.AssertAccessTokenRole { + if c.client.AccessTokenRoleAssertion { return scopes } return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix) @@ -94,15 +92,15 @@ func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []s } func (c *Client) AccessTokenLifetime() time.Duration { - return c.defaultAccessTokenLifetime //PLANNED: impl from real client + return c.client.AccessTokenLifetime } func (c *Client) IDTokenLifetime() time.Duration { - return c.defaultIdTokenLifetime //PLANNED: impl from real client + return c.client.IDTokenLifetime } func (c *Client) AccessTokenType() op.AccessTokenType { - return accessTokenTypeToOIDC(c.app.OIDCConfig.AccessTokenType) + return accessTokenTypeToOIDC(c.client.AccessTokenType) } func (c *Client) IsScopeAllowed(scope string) bool { @@ -127,20 +125,15 @@ func (c *Client) IsScopeAllowed(scope string) bool { if scope == ScopeProjectsRoles { return true } - for _, allowedScope := range c.allowedScopes { - if scope == allowedScope { - return true - } - } - return false + return slices.Contains(c.allowedScopes, scope) } func (c *Client) ClockSkew() time.Duration { - return c.app.OIDCConfig.ClockSkew + return c.client.ClockSkew } func (c *Client) IDTokenUserinfoClaimsAssertion() bool { - return c.app.OIDCConfig.AssertIDTokenUserinfo + return c.client.IDTokenUserinfoAssertion } func accessTokenTypeToOIDC(tokenType domain.OIDCTokenType) op.AccessTokenType { @@ -229,3 +222,14 @@ func removeScopeWithPrefix(scopes []string, scopePrefix ...string) []string { } return newScopeList } + +func clientIDFromCredentials(cc *op.ClientCredentials) (clientID string, assertion bool, err error) { + if cc.ClientAssertion != "" { + claims := new(oidc.JWTTokenRequest) + if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil { + return "", false, oidc.ErrInvalidClient().WithParent(err) + } + return claims.Issuer, true, nil + } + return cc.ClientID, false, nil +} diff --git a/internal/api/oidc/client_credentials.go b/internal/api/oidc/client_credentials.go index 3c2f272ead..b3de23a6f6 100644 --- a/internal/api/oidc/client_credentials.go +++ b/internal/api/oidc/client_credentials.go @@ -1,10 +1,15 @@ package oidc import ( + "context" "time" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query" ) type clientCredentialsRequest struct { @@ -28,15 +33,42 @@ func (c *clientCredentialsRequest) GetScopes() []string { return c.scopes } +func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) { + searchQuery, err := query.NewUserLoginNamesSearchQuery(clientID) + if err != nil { + return nil, err + } + user, err := s.query.GetUser(ctx, false, searchQuery) + if errors.IsNotFound(err) { + return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found") + } + if err != nil { + return nil, err // defaults to server error + } + if user.Machine == nil || user.Machine.Secret == nil { + return nil, errors.ThrowPreconditionFailed(nil, "OIDC-pieP8", "Errors.User.Machine.Secret.NotExisting") + } + if err = crypto.CompareHash(user.Machine.Secret, []byte(clientSecret), s.hashAlg); err != nil { + s.command.MachineSecretCheckFailed(ctx, user.ID, user.ResourceOwner) + return nil, errors.ThrowInvalidArgument(err, "OIDC-VoXo6", "Errors.User.Machine.Secret.Invalid") + } + + s.command.MachineSecretCheckSucceeded(ctx, user.ID, user.ResourceOwner) + return &clientCredentialsClient{ + id: clientID, + user: user, + }, nil +} + type clientCredentialsClient struct { - id string - tokenType op.AccessTokenType + id string + user *query.User } // AccessTokenType returns the AccessTokenType for the token to be created because of the client credentials request // machine users currently only have opaque tokens ([op.AccessTokenTypeBearer]) func (c *clientCredentialsClient) AccessTokenType() op.AccessTokenType { - return c.tokenType + return accessTokenTypeToOIDC(c.user.Machine.AccessTokenType) } // GetID returns the client_id (username of the machine user) for the token to be created because of the client credentials request diff --git a/internal/api/oidc/client_integration_test.go b/internal/api/oidc/client_integration_test.go index 2c3d8e3735..cd31607b3d 100644 --- a/internal/api/oidc/client_integration_test.go +++ b/internal/api/oidc/client_integration_test.go @@ -4,20 +4,26 @@ package oidc_test import ( "context" + "fmt" "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rs" "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" oidc_api "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/management" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user" ) func TestOPStorage_SetUserinfoFromToken(t *testing.T) { @@ -142,3 +148,245 @@ func assertIntrospection( assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"name"]) assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"primary_domain"]) } + +// TestServer_VerifyClient tests verification by running code flow tests +// with clients that have different authentication methods. +func TestServer_VerifyClient(t *testing.T) { + sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) + project, err := Tester.CreateProject(CTX) + require.NoError(t, err) + + inactiveClient, err := Tester.CreateOIDCInactivateClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) + require.NoError(t, err) + nativeClient, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) + require.NoError(t, err) + basicWebClient, err := Tester.CreateOIDCWebClientBasic(CTX, redirectURI, logoutRedirectURI, project.GetId()) + require.NoError(t, err) + jwtWebClient, keyData, err := Tester.CreateOIDCWebClientJWT(CTX, redirectURI, logoutRedirectURI, project.GetId()) + require.NoError(t, err) + + type clientDetails struct { + authReqClientID string + clientID string + clientSecret string + keyData []byte + } + tests := []struct { + name string + client clientDetails + wantErr bool + }{ + { + name: "empty client ID error", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + }, + wantErr: true, + }, + { + name: "client not found error", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + clientID: "foo", + }, + wantErr: true, + }, + { + name: "client inactive error", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + clientID: inactiveClient.GetClientId(), + }, + wantErr: true, + }, + { + name: "native client success", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + clientID: nativeClient.GetClientId(), + }, + }, + { + name: "web client basic secret empty error", + client: clientDetails{ + authReqClientID: basicWebClient.GetClientId(), + clientID: basicWebClient.GetClientId(), + clientSecret: "", + }, + wantErr: true, + }, + { + name: "web client basic secret invalid error", + client: clientDetails{ + authReqClientID: basicWebClient.GetClientId(), + clientID: basicWebClient.GetClientId(), + clientSecret: "wrong", + }, + wantErr: true, + }, + { + name: "web client basic secret success", + client: clientDetails{ + authReqClientID: basicWebClient.GetClientId(), + clientID: basicWebClient.GetClientId(), + clientSecret: basicWebClient.GetClientSecret(), + }, + }, + { + name: "web client JWT profile empty assertion error", + client: clientDetails{ + authReqClientID: jwtWebClient.GetClientId(), + clientID: jwtWebClient.GetClientId(), + }, + wantErr: true, + }, + { + name: "web client JWT profile invalid assertion error", + client: clientDetails{ + authReqClientID: jwtWebClient.GetClientId(), + clientID: jwtWebClient.GetClientId(), + keyData: createInvalidKeyData(t, jwtWebClient), + }, + wantErr: true, + }, + { + name: "web client JWT profile success", + client: clientDetails{ + authReqClientID: jwtWebClient.GetClientId(), + clientID: jwtWebClient.GetClientId(), + keyData: keyData, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fmt.Printf("\n\n%s\n\n", tt.client.keyData) + + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, tt.client.authReqClientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, oidc.ScopeOpenID) + require.NoError(t, err) + linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ + AuthRequestId: authRequestID, + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionID, + SessionToken: sessionToken, + }, + }, + }) + require.NoError(t, err) + + // use a new RP so we can inject different credentials + var options []rp.Option + if tt.client.keyData != nil { + options = append(options, rp.WithJWTProfile(rp.SignerFromKeyFile(tt.client.keyData))) + } + provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), tt.client.clientID, tt.client.clientSecret, redirectURI, []string{oidc.ScopeOpenID}, options...) + require.NoError(t, err) + + // test code exchange + code := assertCodeResponse(t, linkResp.GetCallbackUrl()) + codeOpts := codeExchangeOptions(t, provider) + tokens, err := rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, codeOpts...) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assertTokens(t, tokens, false) + assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) + }) + } +} + +func codeExchangeOptions(t testing.TB, provider rp.RelyingParty) []rp.CodeExchangeOpt { + codeOpts := []rp.CodeExchangeOpt{rp.WithCodeVerifier(integration.CodeVerifier)} + if signer := provider.Signer(); signer != nil { + assertion, err := client.SignedJWTProfileAssertion(provider.OAuthConfig().ClientID, []string{provider.Issuer()}, time.Hour, provider.Signer()) + require.NoError(t, err) + codeOpts = append(codeOpts, rp.WithClientAssertionJWT(assertion)) + } + return codeOpts +} + +func createInvalidKeyData(t testing.TB, client *management.AddOIDCAppResponse) []byte { + key := domain.ApplicationKey{ + Type: domain.AuthNKeyTypeJSON, + KeyID: "1", + PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + ApplicationID: client.GetAppId(), + ClientID: client.GetClientId(), + } + data, err := key.Detail() + require.NoError(t, err) + return data +} + +func TestServer_CreateAccessToken_ClientCredentials(t *testing.T) { + clientID, clientSecret, err := Tester.CreateOIDCCredentialsClient(CTX) + require.NoError(t, err) + + type clientDetails struct { + clientID string + clientSecret string + keyData []byte + } + tests := []struct { + name string + clientID string + clientSecret string + wantErr bool + }{ + { + name: "missing client ID error", + clientID: "", + clientSecret: clientSecret, + wantErr: true, + }, + { + name: "client not found error", + clientID: "foo", + clientSecret: clientSecret, + wantErr: true, + }, + { + name: "machine user without secret error", + clientID: func() string { + name := gofakeit.Username() + _, err := Tester.Client.Mgmt.AddMachineUser(CTX, &management.AddMachineUserRequest{ + Name: name, + UserName: name, + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }) + require.NoError(t, err) + return name + }(), + clientSecret: clientSecret, + wantErr: true, + }, + { + name: "wrong secret error", + clientID: clientID, + clientSecret: "bar", + wantErr: true, + }, + { + name: "success", + clientID: clientID, + clientSecret: clientSecret, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), tt.clientID, tt.clientSecret, redirectURI, []string{oidc.ScopeOpenID}) + require.NoError(t, err) + tokens, err := rp.ClientCredentials(CTX, provider, nil) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotNil(t, tokens) + assert.NotEmpty(t, tokens.AccessToken) + }) + } +} diff --git a/internal/api/oidc/introspect.go b/internal/api/oidc/introspect.go index d48bf001ec..cedac441eb 100644 --- a/internal/api/oidc/introspect.go +++ b/internal/api/oidc/introspect.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/crypto" - errz "github.com/zitadel/zitadel/internal/errors" + zerrors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -31,14 +31,14 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR ctx, cancel := context.WithCancel(ctx) defer cancel() - clientChan := make(chan *instrospectionClientResult) - go s.instrospectionClientAuth(ctx, r.Data.ClientCredentials, clientChan) + clientChan := make(chan *introspectionClientResult) + go s.introspectionClientAuth(ctx, r.Data.ClientCredentials, clientChan) tokenChan := make(chan *introspectionTokenResult) go s.introspectionToken(ctx, r.Data.Token, tokenChan) var ( - client *instrospectionClientResult + client *introspectionClientResult token *introspectionTokenResult ) @@ -116,13 +116,13 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR return op.NewResponse(introspectionResp), nil } -type instrospectionClientResult struct { +type introspectionClientResult struct { clientID string projectID string err error } -func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *instrospectionClientResult) { +func (s *Server) introspectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *introspectionClientResult) { ctx, span := tracing.NewSpan(ctx) clientID, projectID, err := func() (string, string, error) { @@ -147,7 +147,7 @@ func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCred span.EndWithError(err) - rc <- &instrospectionClientResult{ + rc <- &introspectionClientResult{ clientID: clientID, projectID: projectID, err: err, @@ -157,15 +157,11 @@ func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCred // clientFromCredentials parses the client ID early, // and makes a single query for the client for either auth methods. func (s *Server) clientFromCredentials(ctx context.Context, cc *op.ClientCredentials) (client *query.IntrospectionClient, err error) { - if cc.ClientAssertion != "" { - claims := new(oidc.JWTTokenRequest) - if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil { - return nil, oidc.ErrUnauthorizedClient().WithParent(err) - } - client, err = s.query.GetIntrospectionClientByID(ctx, claims.Issuer, true) - } else { - client, err = s.query.GetIntrospectionClientByID(ctx, cc.ClientID, false) + clientID, assertion, err := clientIDFromCredentials(cc) + if err != nil { + return nil, err } + client, err = s.query.GetIntrospectionClientByID(ctx, clientID, assertion) if errors.Is(err, sql.ErrNoRows) { return nil, oidc.ErrUnauthorizedClient().WithParent(err) } @@ -196,5 +192,5 @@ func validateIntrospectionAudience(audience []string, clientID, projectID string return nil } - return errz.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client") + return zerrors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client") } diff --git a/internal/api/oidc/oidc_integration_test.go b/internal/api/oidc/oidc_integration_test.go index e1531fff3b..0e4b6e9c9d 100644 --- a/internal/api/oidc/oidc_integration_test.go +++ b/internal/api/oidc/oidc_integration_test.go @@ -31,9 +31,9 @@ var ( ) const ( - redirectURI = "oidcintegrationtest://callback" + redirectURI = "https://callback" redirectURIImplicit = "http://localhost:9999/callback" - logoutRedirectURI = "oidcintegrationtest://logged-out" + logoutRedirectURI = "https://logged-out" zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix ) diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index cdc167ad21..1f34f62fb0 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -128,6 +128,9 @@ func NewServer( query: query, command: command, keySet: newKeySet(context.TODO(), time.Hour, query.GetActivePublicKeyByID), + defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID), + defaultLoginURLV2: config.DefaultLoginURLV2, + defaultLogoutURLV2: config.DefaultLogoutURLV2, fallbackLogger: fallbackLogger, hashAlg: crypto.NewBCrypt(10), // as we are only verifying in oidc, the cost is already part of the hash string and the config here is irrelevant. signingKeyAlgorithm: config.SigningKeyAlgorithm, diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index 8d04cd62e9..1782966e9c 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -27,6 +27,10 @@ type Server struct { command *command.Commands keySet *keySetCache + defaultLoginURL string + defaultLoginURLV2 string + defaultLogoutURLV2 string + fallbackLogger *slog.Logger hashAlg crypto.HashAlgorithm signingKeyAlgorithm string @@ -143,13 +147,6 @@ func (s *Server) DeviceAuthorization(ctx context.Context, r *op.ClientRequest[oi return s.LegacyServer.DeviceAuthorization(ctx, r) } -func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - return s.LegacyServer.VerifyClient(ctx, r) -} - func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.AccessTokenRequest]) (_ *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/command/command.go b/internal/command/command.go index 3b45a9b874..e8e25203ce 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -9,8 +9,11 @@ import ( "math/big" "net/http" "strconv" + "sync" "time" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" api_http "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" @@ -37,12 +40,15 @@ import ( usr_repo "github.com/zitadel/zitadel/internal/repository/user" usr_grant_repo "github.com/zitadel/zitadel/internal/repository/usergrant" "github.com/zitadel/zitadel/internal/static" + "github.com/zitadel/zitadel/internal/telemetry/tracing" webauthn_helper "github.com/zitadel/zitadel/internal/webauthn" ) type Commands struct { httpClient *http.Client + jobs sync.WaitGroup + checkPermission domain.PermissionCheck newCode cryptoCodeFunc newCodeWithDefault cryptoCodeWithDefaultFunc @@ -257,3 +263,54 @@ func samlCertificateAndKeyGenerator(keySize int) func(id string) ([]byte, []byte return pem.EncodeToMemory(keyBlock), pem.EncodeToMemory(certBlock), nil } } + +// Close blocks until all async jobs are finished, +// the context expires or after eventstore.PushTimeout. +func (c *Commands) Close(ctx context.Context) error { + if c.eventstore.PushTimeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, c.eventstore.PushTimeout) + defer cancel() + } + + done := make(chan struct{}) + go func() { + c.jobs.Wait() + close(done) + }() + select { + case <-done: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// asyncPush attempts to push events to the eventstore in a separate Go routine. +// This can be used to speed up request times when the outcome of the push is +// not important for business logic but have a pure logging function. +// For example this can be used for Secret Check Success and Failed events. +// On push error, a log line describing the error will be emitted. +func (c *Commands) asyncPush(ctx context.Context, cmds ...eventstore.Command) { + // Create a new context, as the request scoped context might get + // canceled before we where able to push. + // The eventstore has its own PushTimeout setting, + // so we don't need to have a context with timeout here. + ctx = context.WithoutCancel(ctx) + + c.jobs.Add(1) + + go func() { + defer c.jobs.Done() + localCtx, span := tracing.NewSpan(ctx) + + _, err := c.eventstore.Push(localCtx, cmds...) + if err != nil { + for _, cmd := range cmds { + logging.WithError(err).Errorf("could not push event %q", cmd.Type()) + } + } + + span.EndWithError(err) + }() +} diff --git a/internal/command/command_test.go b/internal/command/command_test.go index a28f5f890a..2367930b89 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -1,11 +1,19 @@ package command import ( + "context" + "io" + "os" "testing" + "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/repository/user" ) var ( @@ -18,5 +26,121 @@ var ( func TestMain(m *testing.M) { i18n.SupportLanguages(SupportedLanguages...) - m.Run() + os.Exit(m.Run()) +} + +func TestCommands_asyncPush(t *testing.T) { + // make sure the test terminates on deadlock + background := context.Background() + agg := user.NewAggregate("userID", "orgID") + cmd := user.NewMachineSecretCheckFailedEvent(background, &agg.Aggregate) + + tests := []struct { + name string + pushCtx func() (context.Context, context.CancelFunc) + eventstore func(*testing.T) *eventstore.Eventstore + closeCtx func() (context.Context, context.CancelFunc) + wantCloseErr bool + }{ + { + name: "push error", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithCancel(background) + }, + eventstore: expectEventstore( + expectPushFailed(io.ErrClosedPipe, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "success", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithCancel(background) + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "success after push context cancels", + pushCtx: func() (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(background) + cancel() + return ctx, cancel + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "success after push context timeout", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second/100) + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "success after push context timeout", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second/100) + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "close timeout error", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithCancel(background) + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second/100) + }, + wantCloseErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.eventstore(t), + } + c.eventstore.PushTimeout = 10 * time.Second + pushCtx, cancel := tt.pushCtx() + c.asyncPush(pushCtx, cmd) + cancel() + + closeCtx, cancel := tt.closeCtx() + defer cancel() + err := c.Close(closeCtx) + if tt.wantCloseErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + }) + } } diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 7b7ef67d6f..a7c447b2fc 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -95,7 +95,13 @@ func eventPusherToEvents(eventsPushes ...eventstore.Command) []*repository.Event func expectPush(commands ...eventstore.Command) expect { return func(m *mock.MockRepository) { - m.ExpectPush(commands) + m.ExpectPush(commands, 0) + } +} + +func expectPushSlow(sleep time.Duration, commands ...eventstore.Command) expect { + return func(m *mock.MockRepository) { + m.ExpectPush(commands, sleep) } } diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go index ebec62d85f..1be051c5e8 100644 --- a/internal/command/user_machine_secret.go +++ b/internal/command/user_machine_secret.go @@ -3,8 +3,6 @@ package command import ( "context" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -111,59 +109,12 @@ func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation { } } -func (c *Commands) VerifyMachineSecret(ctx context.Context, userID string, resourceOwner string, secret string) (*domain.ObjectDetails, error) { +func (c *Commands) MachineSecretCheckSucceeded(ctx context.Context, userID, resourceOwner string) { agg := user.NewAggregate(userID, resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareVerifyMachineSecret(agg, secret, c.codeAlg)) - if err != nil { - return nil, err - } - - events, err := c.eventstore.Push(ctx, cmds...) - for _, cmd := range cmds { - if cmd.Type() == user.MachineSecretCheckFailedType { - logging.OnError(err).Error("could not push event MachineSecretCheckFailed") - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3kjh", "Errors.User.Machine.Secret.Invalid") - } - } - if err != nil { - return nil, err - } - - return &domain.ObjectDetails{ - Sequence: events[len(events)-1].Sequence(), - EventDate: events[len(events)-1].CreatedAt(), - ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, - }, nil + c.asyncPush(ctx, user.NewMachineSecretCheckSucceededEvent(ctx, &agg.Aggregate)) } -func prepareVerifyMachineSecret(a *user.Aggregate, secret string, algorithm crypto.HashAlgorithm) preparation.Validation { - return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") - } - if a.ID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) - if err != nil { - return nil, err - } - if !isUserStateExists(writeModel.UserState) { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-569sh2o", "Errors.User.NotExisting") - } - if writeModel.ClientSecret == nil { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x8910n", "Errors.User.Machine.Secret.NotExisting") - } - err = crypto.CompareHash(writeModel.ClientSecret, []byte(secret), algorithm) - if err == nil { - return []eventstore.Command{ - user.NewMachineSecretCheckSucceededEvent(ctx, &a.Aggregate), - }, nil - } - return []eventstore.Command{ - user.NewMachineSecretCheckFailedEvent(ctx, &a.Aggregate), - }, nil - }, nil - } +func (c *Commands) MachineSecretCheckFailed(ctx context.Context, userID, resourceOwner string) { + agg := user.NewAggregate(userID, resourceOwner) + c.asyncPush(ctx, user.NewMachineSecretCheckFailedEvent(ctx, &agg.Aggregate)) } diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go index 77343c8fb0..9f7ead9595 100644 --- a/internal/command/user_machine_secret_test.go +++ b/internal/command/user_machine_secret_test.go @@ -3,8 +3,10 @@ package command import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -319,212 +321,34 @@ func TestCommandSide_RemoveMachineSecret(t *testing.T) { } } -func TestCommandSide_VerifyMachineSecret(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - userID string - resourceOwner string - secret string - } - type res struct { - want *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "user invalid, invalid argument error userID", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - userID: "", - resourceOwner: "org1", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "user invalid, invalid argument error resourceowner", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "user not existing, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - err: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "user existing without secret, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "user1", - "username", - "user", - false, - domain.OIDCTokenTypeBearer, - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - err: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "verify machine secret, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "user1", - "username", - "user", - false, - domain.OIDCTokenTypeBearer, - ), - ), - eventFromEventPusher( - user.NewMachineSecretSetEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "bcrypt", - KeyID: "id", - Crypted: []byte("$2a$14$HxC7TAXMeowdqHdSBUfsjOUc0IGajYeApxdYl9lAYC0duZmSkgFia"), - }, - ), - ), - ), - expectPush( - user.NewMachineSecretCheckSucceededEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - secret: "test", - }, - res: res{ - want: &domain.ObjectDetails{ - ResourceOwner: "org1", - }, - }, - }, - { - name: "verify machine secret, failed", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "user1", - "username", - "user", - false, - domain.OIDCTokenTypeBearer, - ), - ), - eventFromEventPusher( - user.NewMachineSecretSetEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "bcrypt", - KeyID: "id", - Crypted: []byte("$2a$14$HxC7TAXMeowdqHdSBUfsjOUc0IGajYeApxdYl9lAYC0duZmSkgFia"), - }, - ), - ), - ), - expectPush( - user.NewMachineSecretCheckFailedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - secret: "wrong", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - codeAlg: crypto.NewBCrypt(14), - } - got, err := r.VerifyMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secret) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) +func TestCommands_MachineSecretCheckSucceeded(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + agg := user.NewAggregate("userID", "orgID") + cmd := user.NewMachineSecretCheckSucceededEvent(ctx, &agg.Aggregate) + + c := &Commands{ + eventstore: eventstoreExpect(t, + expectPushSlow(time.Second/100, cmd), + ), } + c.MachineSecretCheckSucceeded(ctx, "userID", "orgID") + require.NoError(t, c.Close(ctx)) +} + +func TestCommands_MachineSecretCheckFailed(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + agg := user.NewAggregate("userID", "orgID") + cmd := user.NewMachineSecretCheckFailedEvent(ctx, &agg.Aggregate) + + c := &Commands{ + eventstore: eventstoreExpect(t, + expectPushSlow(time.Second/100, cmd), + ), + } + c.MachineSecretCheckFailed(ctx, "userID", "orgID") + require.NoError(t, c.Close(ctx)) } diff --git a/internal/database/database.go b/internal/database/database.go index c889a88420..d88a5f7e06 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -3,6 +3,8 @@ package database import ( "context" "database/sql" + "encoding/json" + "errors" "reflect" "github.com/mitchellh/mapstructure" @@ -11,7 +13,7 @@ import ( _ "github.com/zitadel/zitadel/internal/database/cockroach" "github.com/zitadel/zitadel/internal/database/dialect" _ "github.com/zitadel/zitadel/internal/database/postgres" - "github.com/zitadel/zitadel/internal/errors" + zerrors "github.com/zitadel/zitadel/internal/errors" ) type Config struct { @@ -89,6 +91,24 @@ func (db *DB) QueryRowContext(ctx context.Context, scan func(row *sql.Row) error return row.Err() } +func QueryJSONObject[T any](ctx context.Context, db *DB, query string, args ...any) (*T, error) { + var data []byte + err := db.QueryRowContext(ctx, func(row *sql.Row) error { + return row.Scan(&data) + }, query, args...) + if errors.Is(err, sql.ErrNoRows) { + return nil, err + } + if err != nil { + return nil, zerrors.ThrowInternal(err, "DATAB-Oath6", "Errors.Internal") + } + obj := new(T) + if err = json.Unmarshal(data, obj); err != nil { + return nil, zerrors.ThrowInternal(err, "DATAB-Vohs6", "Errors.Internal") + } + return obj, nil +} + const ( zitadelAppName = "zitadel" EventstorePusherAppName = "zitadel_es_pusher" @@ -106,7 +126,7 @@ func Connect(config Config, useAdmin, isEventPusher bool) (*DB, error) { } if err := client.Ping(); err != nil { - return nil, errors.ThrowPreconditionFailed(err, "DATAB-0pIWD", "Errors.Database.Connection.Failed") + return nil, zerrors.ThrowPreconditionFailed(err, "DATAB-0pIWD", "Errors.Database.Connection.Failed") } return &DB{ diff --git a/internal/database/database_test.go b/internal/database/database_test.go new file mode 100644 index 0000000000..f7d63d6d64 --- /dev/null +++ b/internal/database/database_test.go @@ -0,0 +1,92 @@ +package database + +import ( + "context" + "database/sql" + "database/sql/driver" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database/mock" + zerrors "github.com/zitadel/zitadel/internal/errors" +) + +func TestQueryJSONObject(t *testing.T) { + type dst struct { + A int `json:"a,omitempty"` + } + const ( + query = `select $1;` + arg = 1 + ) + + tests := []struct { + name string + mock func(*testing.T) *mock.SQLMock + want *dst + wantErr error + }{ + { + name: "tx error", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, mock.ExpectBegin(sql.ErrConnDone)) + }, + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "DATAB-Oath6", "Errors.Internal"), + }, + { + name: "no rows", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectBegin(nil), + mock.ExpectQuery(query, + mock.WithQueryArgs(arg), + mock.WithQueryResult([]string{"json"}, [][]driver.Value{}), + ), + ) + }, + wantErr: sql.ErrNoRows, + }, + { + name: "unmarshal error", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectBegin(nil), + mock.ExpectQuery(query, + mock.WithQueryArgs(arg), + mock.WithQueryResult([]string{"json"}, [][]driver.Value{{`~~~`}}), + ), + mock.ExpectCommit(nil), + ) + }, + wantErr: zerrors.ThrowInternal(nil, "DATAB-Vohs6", "Errors.Internal"), + }, + { + name: "success", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectBegin(nil), + mock.ExpectQuery(query, + mock.WithQueryArgs(arg), + mock.WithQueryResult([]string{"json"}, [][]driver.Value{{`{"a":1}`}}), + ), + mock.ExpectCommit(nil), + ) + }, + want: &dst{A: 1}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := tt.mock(t) + defer mock.Assert(t) + db := &DB{ + DB: mock.DB, + } + got, err := QueryJSONObject[dst](context.Background(), db, query, arg) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/database/mock/sql_mock.go b/internal/database/mock/sql_mock.go index 07d74151e0..e05b188afc 100644 --- a/internal/database/mock/sql_mock.go +++ b/internal/database/mock/sql_mock.go @@ -53,6 +53,15 @@ func ExpectBegin(err error) expectation { } } +func ExpectCommit(err error) expectation { + return func(m sqlmock.Sqlmock) { + e := m.ExpectCommit() + if err != nil { + e.WillReturnError(err) + } + } +} + type ExecOpt func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec func WithExecArgs(args ...driver.Value) ExecOpt { diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index bf49929c7b..6ae64ddf0f 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -75,11 +75,15 @@ func (m *MockRepository) ExpectInstanceIDsError(err error) *MockRepository { return m } -func (m *MockRepository) ExpectPush(expectedCommands []eventstore.Command) *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.ctrl.T.Helper() + time.Sleep(sleep) + if len(expectedCommands) != len(commands) { return nil, fmt.Errorf("unexpected amount of commands: want %d, got %d", len(expectedCommands), len(commands)) } diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index b6edcd3aea..aaf3ff1e31 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -8,26 +8,30 @@ import ( "strings" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rs" "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/protobuf/types/known/timestamppb" http_util "github.com/zitadel/zitadel/internal/api/http" oidc_internal "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/pkg/grpc/app" + "github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/user" ) -func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { +func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType) (*management.AddOIDCAppResponse, error) { return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: projectID, Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), RedirectUris: []string{redirectURI}, ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN}, - AppType: app.OIDCAppType_OIDC_APP_TYPE_NATIVE, - AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, + AppType: appType, + AuthMethodType: authMethod, PostLogoutRedirectUris: []string{logoutRedirectURI}, Version: app.OIDCVersion_OIDC_VERSION_1_0, DevMode: false, @@ -41,6 +45,46 @@ func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logout }) } +func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { + return s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE) +} + +func (s *Tester) CreateOIDCWebClientBasic(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { + return s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC) +} + +func (s *Tester) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (client *management.AddOIDCAppResponse, keyData []byte, err error) { + client, err = s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT) + if err != nil { + return nil, nil, err + } + key, err := s.Client.Mgmt.AddAppKey(ctx, &management.AddAppKeyRequest{ + ProjectId: projectID, + AppId: client.GetAppId(), + Type: authn.KeyType_KEY_TYPE_JSON, + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour)), + }) + if err != nil { + return nil, nil, err + } + return client, key.GetKeyDetails(), nil +} + +func (s *Tester) CreateOIDCInactivateClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { + client, err := s.CreateOIDCNativeClient(ctx, redirectURI, logoutRedirectURI, projectID) + if err != nil { + return nil, err + } + _, err = s.Client.Mgmt.DeactivateApp(ctx, &management.DeactivateAppRequest{ + ProjectId: projectID, + AppId: client.GetAppId(), + }) + if err != nil { + return nil, err + } + return client, err +} + func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) { project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), @@ -83,14 +127,14 @@ func (s *Tester) CreateAPIClient(ctx context.Context, projectID string) (*manage }) } +const CodeVerifier = "codeVerifier" + func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...) if err != nil { return "", err } - - codeVerifier := "codeVerifier" - codeChallenge := oidc.NewSHACodeChallenge(codeVerifier) + codeChallenge := oidc.NewSHACodeChallenge(CodeVerifier) authURL := rp.AuthURL("state", provider, rp.WithCodeChallenge(codeChallenge)) req, err := GetRequest(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient}) @@ -196,3 +240,22 @@ func CheckRedirect(req *http.Request) (*url.URL, error) { return resp.Location() } + +func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (string, string, error) { + name := gofakeit.Username() + user, err := s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ + Name: name, + UserName: name, + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }) + if err != nil { + return "", "", err + } + secret, err := s.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{ + UserId: user.GetUserId(), + }) + if err != nil { + return "", "", err + } + return secret.GetClientId(), secret.GetClientSecret(), nil +} diff --git a/internal/notification/channels/smtp/channel.go b/internal/notification/channels/smtp/channel.go index ceea5c9dfb..35da2b3e1c 100644 --- a/internal/notification/channels/smtp/channel.go +++ b/internal/notification/channels/smtp/channel.go @@ -2,10 +2,10 @@ package smtp import ( "crypto/tls" + "errors" "net" "net/smtp" - "github.com/pkg/errors" "github.com/zitadel/logging" caos_errs "github.com/zitadel/zitadel/internal/errors" diff --git a/internal/query/embed/oidc_client_by_id.sql b/internal/query/embed/oidc_client_by_id.sql new file mode 100644 index 0000000000..64986a42da --- /dev/null +++ b/internal/query/embed/oidc_client_by_id.sql @@ -0,0 +1,46 @@ +--deallocate q; +--prepare q(text, text, boolean) as + +with client as ( + select c.instance_id, + c.app_id, c.client_id, c.client_secret, c.redirect_uris, c.response_types, c.grant_types, + c.application_type, c.auth_method_type, c.post_logout_redirect_uris, c.is_dev_mode, + c.access_token_type, c.access_token_role_assertion, c.id_token_role_assertion, + c.id_token_userinfo_assertion, c.clock_skew, c.additional_origins, a.project_id, a.state + from projections.apps6_oidc_configs c + join projections.apps6 a on a.id = c.app_id and a.instance_id = c.instance_id + where c.instance_id = $1 + and c.client_id = $2 +), +roles as ( + select p.project_id, json_agg(p.role_key) as project_role_keys + from projections.project_roles4 p + join client c on c.project_id = p.project_id + and p.instance_id = c.instance_id + group by p.project_id +), +keys as ( + select identifier as client_id, json_object_agg(id, encode(public_key, 'base64')) as public_keys + from projections.authn_keys2 + where $3 = true -- when argument is false, don't waste time on trying to query for keys. + and instance_id = $1 + and identifier = $2 + and expiration > current_timestamp + group by identifier +), +settings as ( + select instance_id, access_token_lifetime, id_token_lifetime + from projections.oidc_settings2 + where aggregate_id = $1 + and instance_id = $1 +) + +select row_to_json(r) as client from ( + select c.*, r.project_role_keys, k.public_keys, s.access_token_lifetime, s.id_token_lifetime + from client c + left join roles r on r.project_id = c.project_id + left join keys k on k.client_id = c.client_id + join settings s on s.instance_id = s.instance_id +) r; + +--execute q('230690539048009730', '236647088211951618@tests', true); \ No newline at end of file diff --git a/internal/query/embed/userinfo_by_id.sql b/internal/query/embed/userinfo_by_id.sql index 1f289f60c9..ad959e43f0 100644 --- a/internal/query/embed/userinfo_by_id.sql +++ b/internal/query/embed/userinfo_by_id.sql @@ -1,6 +1,6 @@ with usr as ( select u.id, u.creation_date, u.change_date, u.sequence, u.state, u.resource_owner, u.username, n.login_name as preferred_login_name - from projections.users9 u + from projections.users10 u left join projections.login_names3 n on u.id = n.user_id and u.instance_id = n.instance_id where u.id = $1 and u.instance_id = $2 @@ -9,7 +9,7 @@ with usr as ( human as ( select $1 as user_id, row_to_json(r) as human from ( select first_name, last_name, nick_name, display_name, avatar_key, preferred_language, gender, email, is_email_verified, phone, is_phone_verified - from projections.users9_humans + from projections.users10_humans where user_id = $1 and instance_id = $2 ) r @@ -17,7 +17,7 @@ human as ( machine as ( select $1 as user_id, row_to_json(r) as machine from ( select name, description - from projections.users9_machines + from projections.users10_machines where user_id = $1 and instance_id = $2 ) r diff --git a/internal/query/iam_member_test.go b/internal/query/iam_member_test.go index 476e4e2358..8f1857eae9 100644 --- a/internal/query/iam_member_test.go +++ b/internal/query/iam_member_test.go @@ -21,21 +21,21 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names3.login_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.display_name" + - ", projections.users9_machines.name" + - ", projections.users9_humans.avatar_key" + - ", projections.users9.type" + + ", projections.users10_humans.email" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.display_name" + + ", projections.users10_machines.name" + + ", projections.users10_humans.avatar_key" + + ", projections.users10.type" + ", COUNT(*) OVER () " + "FROM projections.instance_members4 AS members " + - "LEFT JOIN projections.users9_humans " + - "ON members.user_id = projections.users9_humans.user_id AND members.instance_id = projections.users9_humans.instance_id " + - "LEFT JOIN projections.users9_machines " + - "ON members.user_id = projections.users9_machines.user_id AND members.instance_id = projections.users9_machines.instance_id " + - "LEFT JOIN projections.users9 " + - "ON members.user_id = projections.users9.id AND members.instance_id = projections.users9.instance_id " + + "LEFT JOIN projections.users10_humans " + + "ON members.user_id = projections.users10_humans.user_id AND members.instance_id = projections.users10_humans.instance_id " + + "LEFT JOIN projections.users10_machines " + + "ON members.user_id = projections.users10_machines.user_id AND members.instance_id = projections.users10_machines.instance_id " + + "LEFT JOIN projections.users10 " + + "ON members.user_id = projections.users10.id AND members.instance_id = projections.users10.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id AND members.instance_id = projections.login_names3.instance_id " + "AS OF SYSTEM TIME '-1 ms' " + diff --git a/internal/query/oidc_client.go b/internal/query/oidc_client.go new file mode 100644 index 0000000000..60ecf25174 --- /dev/null +++ b/internal/query/oidc_client.go @@ -0,0 +1,61 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "errors" + "time" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + zerrors "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +type OIDCClient struct { + InstanceID string `json:"instance_id,omitempty"` + AppID string `json:"app_id,omitempty"` + State domain.AppState `json:"state,omitempty"` + ClientID string `json:"client_id,omitempty"` + ClientSecret *crypto.CryptoValue `json:"client_secret,omitempty"` + RedirectURIs []string `json:"redirect_uris,omitempty"` + ResponseTypes []domain.OIDCResponseType `json:"response_types,omitempty"` + GrantTypes []domain.OIDCGrantType `json:"grant_types,omitempty"` + ApplicationType domain.OIDCApplicationType `json:"application_type,omitempty"` + AuthMethodType domain.OIDCAuthMethodType `json:"auth_method_type,omitempty"` + PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris,omitempty"` + IsDevMode bool `json:"is_dev_mode,omitempty"` + AccessTokenType domain.OIDCTokenType `json:"access_token_type,omitempty"` + AccessTokenRoleAssertion bool `json:"access_token_role_assertion,omitempty"` + IDTokenRoleAssertion bool `json:"id_token_role_assertion,omitempty"` + IDTokenUserinfoAssertion bool `json:"id_token_userinfo_assertion,omitempty"` + ClockSkew time.Duration `json:"clock_skew,omitempty"` + AdditionalOrigins []string `json:"additional_origins,omitempty"` + PublicKeys map[string][]byte `json:"public_keys,omitempty"` + ProjectID string `json:"project_id,omitempty"` + ProjectRoleKeys []string `json:"project_role_keys,omitempty"` + AccessTokenLifetime time.Duration `json:"access_token_lifetime,omitempty"` + IDTokenLifetime time.Duration `json:"id_token_lifetime,omitempty"` +} + +//go:embed embed/oidc_client_by_id.sql +var oidcClientQuery string + +func (q *Queries) GetOIDCClientByID(ctx context.Context, clientID string, getKeys bool) (client *OIDCClient, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + client, err = database.QueryJSONObject[OIDCClient](ctx, q.client, oidcClientQuery, + authz.GetInstance(ctx).InstanceID(), clientID, getKeys, + ) + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-wu6Ee", "Errors.App.NotFound") + } + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-ieR7R", "Errors.Internal") + } + return client, err +} diff --git a/internal/query/oidc_client_test.go b/internal/query/oidc_client_test.go new file mode 100644 index 0000000000..0593e1ce03 --- /dev/null +++ b/internal/query/oidc_client_test.go @@ -0,0 +1,167 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + _ "embed" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + zerrors "github.com/zitadel/zitadel/internal/errors" +) + +var ( + //go:embed testdata/oidc_client_jwt.json + testdataOidcClientJWT string + //go:embed testdata/oidc_client_public.json + testdataOidcClientPublic string + //go:embed testdata/oidc_client_secret.json + testdataOidcClientSecret string +) + +func TestQueries_GetOIDCClientByID(t *testing.T) { + expQuery := regexp.QuoteMeta(oidcClientQuery) + cols := []string{"client"} + pubkey := `-----BEGIN RSA PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ufAL1b72bIy1ar+Ws6b +GohJJQFB7dfRapDqeqM8Ukp6CVdPzq/pOz1viAq50yzWZJryF+2wshFAKGF9A2/B +2Yf9bJXPZ/KbkFrYT3NTvYDkvlaSTl9mMnzrU29s48F1PTWKfB+C3aMsOEG1BufV +s63qF4nrEPjSbhljIco9FZq4XppIzhMQ0fDdA/+XygCJqvuaL0LibM1KrlUdnu71 +YekhSJjEPnvOisXIk4IXywoGIOwtjxkDvNItQvaMVldr4/kb6uvbgdWwq5EwBZXq +low2kyJov38V4Uk2I8kuXpLcnrpw5Tio2ooiUE27b0vHZqBKOei9Uo88qCrn3EKx +6QIDAQAB +-----END RSA PUBLIC KEY----- +` + + tests := []struct { + name string + mock sqlExpectation + want *OIDCClient + wantErr error + }{ + { + name: "no rows", + mock: mockQueryErr(expQuery, sql.ErrNoRows, "instanceID", "clientID", true), + wantErr: zerrors.ThrowNotFound(sql.ErrNoRows, "QUERY-wu6Ee", "Errors.App.NotFound"), + }, + { + name: "internal error", + mock: mockQueryErr(expQuery, sql.ErrConnDone, "instanceID", "clientID", true), + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-ieR7R", "Errors.Internal"), + }, + { + name: "jwt client", + mock: mockQuery(expQuery, cols, []driver.Value{testdataOidcClientJWT}, "instanceID", "clientID", true), + want: &OIDCClient{ + InstanceID: "230690539048009730", + AppID: "236647088211886082", + State: domain.AppStateActive, + ClientID: "236647088211951618@tests", + ClientSecret: nil, + RedirectURIs: []string{"http://localhost:9999/auth/callback"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode, domain.OIDCGrantTypeRefreshToken}, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypePrivateKeyJWT, + PostLogoutRedirectURIs: []string{"https://example.com/logout"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AccessTokenRoleAssertion: true, + IDTokenRoleAssertion: true, + IDTokenUserinfoAssertion: true, + ClockSkew: 1000000000, + AdditionalOrigins: []string{"https://example.com"}, + ProjectID: "236645808328409090", + PublicKeys: map[string][]byte{"236647201860747266": []byte(pubkey)}, + ProjectRoleKeys: []string{"role1", "role2"}, + AccessTokenLifetime: 43200000000000, + IDTokenLifetime: 43200000000000, + }, + }, + { + name: "public client", + mock: mockQuery(expQuery, cols, []driver.Value{testdataOidcClientPublic}, "instanceID", "clientID", true), + want: &OIDCClient{ + InstanceID: "230690539048009730", + AppID: "236646457053020162", + State: domain.AppStateActive, + ClientID: "236646457053085698@tests", + ClientSecret: nil, + RedirectURIs: []string{"http://localhost:9999/auth/callback"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: nil, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: 0, + AdditionalOrigins: nil, + PublicKeys: nil, + ProjectID: "236645808328409090", + ProjectRoleKeys: []string{"role1", "role2"}, + AccessTokenLifetime: 43200000000000, + IDTokenLifetime: 43200000000000, + }, + }, + { + name: "secret client", + mock: mockQuery(expQuery, cols, []driver.Value{testdataOidcClientSecret}, "instanceID", "clientID", true), + want: &OIDCClient{ + InstanceID: "230690539048009730", + AppID: "236646858984783874", + State: domain.AppStateActive, + ClientID: "236646858984849410@tests", + ClientSecret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "bcrypt", + Crypted: []byte(`$2a$14$OzZ0XEZZEtD13py/EPba2evsS6WcKZ5orVMj9pWHEGEHmLu2h3PFq`), + }, + RedirectURIs: []string{"http://localhost:9999/auth/callback"}, + ResponseTypes: []domain.OIDCResponseType{0}, + GrantTypes: []domain.OIDCGrantType{0}, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypeBasic, + PostLogoutRedirectURIs: nil, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: 0, + AdditionalOrigins: nil, + PublicKeys: nil, + ProjectID: "236645808328409090", + ProjectRoleKeys: []string{"role1", "role2"}, + AccessTokenLifetime: 43200000000000, + IDTokenLifetime: 43200000000000, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + execMock(t, tt.mock, func(db *sql.DB) { + q := &Queries{ + client: &database.DB{ + DB: db, + Database: &prepareDB{}, + }, + } + ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") + got, err := q.GetOIDCClientByID(ctx, "clientID", true) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + }) + } +} diff --git a/internal/query/org_member_test.go b/internal/query/org_member_test.go index 7dc80dffae..37443d4dc9 100644 --- a/internal/query/org_member_test.go +++ b/internal/query/org_member_test.go @@ -21,24 +21,24 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names3.login_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.display_name" + - ", projections.users9_machines.name" + - ", projections.users9_humans.avatar_key" + - ", projections.users9.type" + + ", projections.users10_humans.email" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.display_name" + + ", projections.users10_machines.name" + + ", projections.users10_humans.avatar_key" + + ", projections.users10.type" + ", COUNT(*) OVER () " + "FROM projections.org_members4 AS members " + - "LEFT JOIN projections.users9_humans " + - "ON members.user_id = projections.users9_humans.user_id " + - "AND members.instance_id = projections.users9_humans.instance_id " + - "LEFT JOIN projections.users9_machines " + - "ON members.user_id = projections.users9_machines.user_id " + - "AND members.instance_id = projections.users9_machines.instance_id " + - "LEFT JOIN projections.users9 " + - "ON members.user_id = projections.users9.id " + - "AND members.instance_id = projections.users9.instance_id " + + "LEFT JOIN projections.users10_humans " + + "ON members.user_id = projections.users10_humans.user_id " + + "AND members.instance_id = projections.users10_humans.instance_id " + + "LEFT JOIN projections.users10_machines " + + "ON members.user_id = projections.users10_machines.user_id " + + "AND members.instance_id = projections.users10_machines.instance_id " + + "LEFT JOIN projections.users10 " + + "ON members.user_id = projections.users10.id " + + "AND members.instance_id = projections.users10.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index 2cf413913c..91cd210679 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -21,24 +21,24 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names3.login_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.display_name" + - ", projections.users9_machines.name" + - ", projections.users9_humans.avatar_key" + - ", projections.users9.type" + + ", projections.users10_humans.email" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.display_name" + + ", projections.users10_machines.name" + + ", projections.users10_humans.avatar_key" + + ", projections.users10.type" + ", COUNT(*) OVER () " + "FROM projections.project_grant_members4 AS members " + - "LEFT JOIN projections.users9_humans " + - "ON members.user_id = projections.users9_humans.user_id " + - "AND members.instance_id = projections.users9_humans.instance_id " + - "LEFT JOIN projections.users9_machines " + - "ON members.user_id = projections.users9_machines.user_id " + - "AND members.instance_id = projections.users9_machines.instance_id " + - "LEFT JOIN projections.users9 " + - "ON members.user_id = projections.users9.id " + - "AND members.instance_id = projections.users9.instance_id " + + "LEFT JOIN projections.users10_humans " + + "ON members.user_id = projections.users10_humans.user_id " + + "AND members.instance_id = projections.users10_humans.instance_id " + + "LEFT JOIN projections.users10_machines " + + "ON members.user_id = projections.users10_machines.user_id " + + "AND members.instance_id = projections.users10_machines.instance_id " + + "LEFT JOIN projections.users10 " + + "ON members.user_id = projections.users10.id " + + "AND members.instance_id = projections.users10.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + diff --git a/internal/query/project_member_test.go b/internal/query/project_member_test.go index b280750247..defec46d49 100644 --- a/internal/query/project_member_test.go +++ b/internal/query/project_member_test.go @@ -21,24 +21,24 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names3.login_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.display_name" + - ", projections.users9_machines.name" + - ", projections.users9_humans.avatar_key" + - ", projections.users9.type" + + ", projections.users10_humans.email" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.display_name" + + ", projections.users10_machines.name" + + ", projections.users10_humans.avatar_key" + + ", projections.users10.type" + ", COUNT(*) OVER () " + "FROM projections.project_members4 AS members " + - "LEFT JOIN projections.users9_humans " + - "ON members.user_id = projections.users9_humans.user_id " + - "AND members.instance_id = projections.users9_humans.instance_id " + - "LEFT JOIN projections.users9_machines " + - "ON members.user_id = projections.users9_machines.user_id " + - "AND members.instance_id = projections.users9_machines.instance_id " + - "LEFT JOIN projections.users9 " + - "ON members.user_id = projections.users9.id " + - "AND members.instance_id = projections.users9.instance_id " + + "LEFT JOIN projections.users10_humans " + + "ON members.user_id = projections.users10_humans.user_id " + + "AND members.instance_id = projections.users10_humans.instance_id " + + "LEFT JOIN projections.users10_machines " + + "ON members.user_id = projections.users10_machines.user_id " + + "AND members.instance_id = projections.users10_machines.instance_id " + + "LEFT JOIN projections.users10 " + + "ON members.user_id = projections.users10.id " + + "AND members.instance_id = projections.users10.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index 3451e6fc37..d9813f1801 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -15,7 +15,7 @@ import ( ) const ( - UserTable = "projections.users9" + UserTable = "projections.users10" UserHumanTable = UserTable + "_" + UserHumanSuffix UserMachineTable = UserTable + "_" + UserMachineSuffix UserNotifyTable = UserTable + "_" + UserNotifySuffix @@ -57,7 +57,7 @@ const ( MachineUserInstanceIDCol = "instance_id" MachineNameCol = "name" MachineDescriptionCol = "description" - MachineHasSecretCol = "has_secret" + MachineSecretCol = "secret" MachineAccessTokenTypeCol = "access_token_type" // notify @@ -122,7 +122,7 @@ func (*userProjection) Init() *old_handler.Check { handler.NewColumn(MachineUserInstanceIDCol, handler.ColumnTypeText), handler.NewColumn(MachineNameCol, handler.ColumnTypeText), handler.NewColumn(MachineDescriptionCol, handler.ColumnTypeText, handler.Nullable()), - handler.NewColumn(MachineHasSecretCol, handler.ColumnTypeBool, handler.Default(false)), + handler.NewColumn(MachineSecretCol, handler.ColumnTypeJSONB, handler.Nullable()), handler.NewColumn(MachineAccessTokenTypeCol, handler.ColumnTypeEnum, handler.Default(0)), }, handler.NewPrimaryKey(MachineUserInstanceIDCol, MachineUserIDCol), @@ -936,7 +936,7 @@ func (p *userProjection) reduceMachineSecretSet(event eventstore.Event) (*handle ), handler.AddUpdateStatement( []handler.Column{ - handler.NewCol(MachineHasSecretCol, true), + handler.NewCol(MachineSecretCol, e.ClientSecret), }, []handler.Condition{ handler.NewCond(MachineUserIDCol, e.Aggregate().ID), @@ -967,7 +967,7 @@ func (p *userProjection) reduceMachineSecretRemoved(event eventstore.Event) (*ha ), handler.AddUpdateStatement( []handler.Column{ - handler.NewCol(MachineHasSecretCol, false), + handler.NewCol(MachineSecretCol, nil), }, []handler.Condition{ handler.NewCond(MachineUserIDCol, e.Aggregate().ID), diff --git a/internal/query/projection/user_test.go b/internal/query/projection/user_test.go index da4f2a860c..fe0a8dad4f 100644 --- a/internal/query/projection/user_test.go +++ b/internal/query/projection/user_test.go @@ -4,6 +4,7 @@ import ( "database/sql" "testing" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -50,7 +51,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -64,7 +65,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -79,7 +80,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -119,7 +120,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -133,7 +134,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -148,7 +149,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -183,7 +184,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -197,7 +198,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -212,7 +213,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -252,7 +253,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -266,7 +267,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -281,7 +282,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -321,7 +322,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -335,7 +336,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -350,7 +351,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -385,7 +386,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -399,7 +400,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -414,7 +415,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -444,7 +445,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -472,7 +473,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -500,7 +501,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -528,7 +529,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -556,7 +557,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateLocked, @@ -586,7 +587,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -616,7 +617,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateInactive, @@ -646,7 +647,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -676,7 +677,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users9 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.users10 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -705,7 +706,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "username", @@ -737,7 +738,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "id@temporary.domain", @@ -774,7 +775,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -783,7 +784,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users10_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -823,7 +824,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -832,7 +833,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users10_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -867,7 +868,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -876,7 +877,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ domain.PhoneNumber("+41 00 000 00 00"), false, @@ -885,7 +886,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "+41 00 000 00 00", Valid: true}, "agg-id", @@ -915,7 +916,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -924,7 +925,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ domain.PhoneNumber("+41 00 000 00 00"), false, @@ -933,7 +934,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "+41 00 000 00 00", Valid: true}, "agg-id", @@ -961,7 +962,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -970,7 +971,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -979,7 +980,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1008,7 +1009,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1017,7 +1018,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1026,7 +1027,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1055,7 +1056,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1064,7 +1065,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1072,7 +1073,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users10_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1099,7 +1100,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1108,7 +1109,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1116,7 +1117,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users10_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1145,7 +1146,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1154,7 +1155,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ domain.EmailAddress("email@zitadel.com"), false, @@ -1163,7 +1164,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "email@zitadel.com", Valid: true}, "agg-id", @@ -1193,7 +1194,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1202,7 +1203,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ domain.EmailAddress("email@zitadel.com"), false, @@ -1211,7 +1212,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "email@zitadel.com", Valid: true}, "agg-id", @@ -1239,7 +1240,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1248,7 +1249,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1256,7 +1257,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users10_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1283,7 +1284,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1292,7 +1293,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1300,7 +1301,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users10_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1329,7 +1330,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1338,7 +1339,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "users/agg-id/avatar", "agg-id", @@ -1366,7 +1367,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1375,7 +1376,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ nil, "agg-id", @@ -1406,7 +1407,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1420,7 +1421,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_machines (user_id, instance_id, name, description, access_token_type) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_machines (user_id, instance_id, name, description, access_token_type) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1454,7 +1455,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1468,7 +1469,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_machines (user_id, instance_id, name, description, access_token_type) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_machines (user_id, instance_id, name, description, access_token_type) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1501,7 +1502,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1510,7 +1511,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "machine-name", "description", @@ -1541,7 +1542,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1550,7 +1551,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET name = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_machines SET name = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "machine-name", "agg-id", @@ -1580,7 +1581,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1589,7 +1590,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET description = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_machines SET description = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "description", "agg-id", @@ -1627,7 +1628,7 @@ func TestUserProjection_reduces(t *testing.T) { user.MachineSecretSetType, user.AggregateType, []byte(`{ - "client_secret": {} + "clientSecret": {"CryptoType":1,"Algorithm":"bcrypt","Crypted":"deadbeef"} }`), ), user.MachineSecretSetEventMapper), }, @@ -1638,7 +1639,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1647,9 +1648,13 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET has_secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_machines SET secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ - true, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "bcrypt", + Crypted: []byte{117, 230, 157, 109, 231, 159}, + }, "agg-id", "instance-id", }, @@ -1659,7 +1664,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - name: "reduceMachineSecretSet", + name: "reduceMachineSecretRemoved", args: args{ event: getEvent( testEvent( @@ -1675,7 +1680,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1684,9 +1689,9 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET has_secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_machines SET secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ - false, + nil, "agg-id", "instance-id", }, @@ -1712,7 +1717,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users9 WHERE (instance_id = $1) AND (resource_owner = $2)", + expectedStmt: "DELETE FROM projections.users10 WHERE (instance_id = $1) AND (resource_owner = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", @@ -1739,7 +1744,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users9 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.users10 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/quota_notifications.go b/internal/query/quota_notifications.go index 7fc0748f63..fb85f9373e 100644 --- a/internal/query/quota_notifications.go +++ b/internal/query/quota_notifications.go @@ -3,12 +3,11 @@ package query import ( "context" "database/sql" - errs "errors" + "errors" "math" "time" sq "github.com/Masterminds/squirrel" - "github.com/pkg/errors" "github.com/zitadel/zitadel/internal/api/call" zitadel_errors "github.com/zitadel/zitadel/internal/errors" @@ -166,7 +165,7 @@ func prepareQuotaNotificationsQuery(ctx context.Context, db prepareDatabase) (sq var nextDueThreshold sql.NullInt16 err := rows.Scan(&cfg.ID, &cfg.CallURL, &cfg.Percent, &cfg.Repeat, &nextDueThreshold) if err != nil { - if errs.Is(err, sql.ErrNoRows) { + if errors.Is(err, sql.ErrNoRows) { return nil, zitadel_errors.ThrowNotFound(err, "QUERY-bbqWb", "Errors.QuotaNotification.NotExisting") } return nil, zitadel_errors.ThrowInternal(err, "QUERY-8copS", "Errors.Internal") diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index d4f621073a..2bb90bbca1 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -31,7 +31,7 @@ var ( ` projections.sessions8.user_resource_owner,` + ` projections.sessions8.user_checked_at,` + ` projections.login_names3.login_name,` + - ` projections.users9_humans.display_name,` + + ` projections.users10_humans.display_name,` + ` projections.sessions8.password_checked_at,` + ` projections.sessions8.intent_checked_at,` + ` projections.sessions8.webauthn_checked_at,` + @@ -48,8 +48,8 @@ var ( ` projections.sessions8.expiration` + ` FROM projections.sessions8` + ` LEFT JOIN projections.login_names3 ON projections.sessions8.user_id = projections.login_names3.user_id AND projections.sessions8.instance_id = projections.login_names3.instance_id` + - ` LEFT JOIN projections.users9_humans ON projections.sessions8.user_id = projections.users9_humans.user_id AND projections.sessions8.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9 ON projections.sessions8.user_id = projections.users9.id AND projections.sessions8.instance_id = projections.users9.instance_id` + + ` LEFT JOIN projections.users10_humans ON projections.sessions8.user_id = projections.users10_humans.user_id AND projections.sessions8.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10 ON projections.sessions8.user_id = projections.users10.id AND projections.sessions8.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions8.id,` + ` projections.sessions8.creation_date,` + @@ -62,7 +62,7 @@ var ( ` projections.sessions8.user_resource_owner,` + ` projections.sessions8.user_checked_at,` + ` projections.login_names3.login_name,` + - ` projections.users9_humans.display_name,` + + ` projections.users10_humans.display_name,` + ` projections.sessions8.password_checked_at,` + ` projections.sessions8.intent_checked_at,` + ` projections.sessions8.webauthn_checked_at,` + @@ -75,8 +75,8 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.sessions8` + ` LEFT JOIN projections.login_names3 ON projections.sessions8.user_id = projections.login_names3.user_id AND projections.sessions8.instance_id = projections.login_names3.instance_id` + - ` LEFT JOIN projections.users9_humans ON projections.sessions8.user_id = projections.users9_humans.user_id AND projections.sessions8.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9 ON projections.sessions8.user_id = projections.users9.id AND projections.sessions8.instance_id = projections.users9.instance_id` + + ` LEFT JOIN projections.users10_humans ON projections.sessions8.user_id = projections.users10_humans.user_id AND projections.sessions8.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10 ON projections.sessions8.user_id = projections.users10.id AND projections.sessions8.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) sessionCols = []string{ diff --git a/internal/query/testdata/oidc_client_jwt.json b/internal/query/testdata/oidc_client_jwt.json new file mode 100644 index 0000000000..d32e5a5110 --- /dev/null +++ b/internal/query/testdata/oidc_client_jwt.json @@ -0,0 +1,27 @@ +{ + "instance_id": "230690539048009730", + "app_id": "236647088211886082", + "client_id": "236647088211951618@tests", + "client_secret": null, + "redirect_uris": ["http://localhost:9999/auth/callback"], + "response_types": [0], + "grant_types": [0, 2], + "application_type": 0, + "auth_method_type": 3, + "post_logout_redirect_uris": ["https://example.com/logout"], + "is_dev_mode": true, + "access_token_type": 1, + "access_token_role_assertion": true, + "id_token_role_assertion": true, + "id_token_userinfo_assertion": true, + "clock_skew": 1000000000, + "additional_origins": ["https://example.com"], + "project_id": "236645808328409090", + "state": 1, + "project_role_keys": ["role1", "role2"], + "public_keys": { + "236647201860747266": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFB\nT0NBUThBTUlJQkNnS0NBUUVBMnVmQUwxYjcyYkl5MWFyK1dzNmIKR29oSkpRRkI3ZGZSYXBEcWVx\nTThVa3A2Q1ZkUHpxL3BPejF2aUFxNTB5eldaSnJ5Risyd3NoRkFLR0Y5QTIvQgoyWWY5YkpYUFov\nS2JrRnJZVDNOVHZZRGt2bGFTVGw5bU1uenJVMjlzNDhGMVBUV0tmQitDM2FNc09FRzFCdWZWCnM2\nM3FGNG5yRVBqU2JobGpJY285RlpxNFhwcEl6aE1RMGZEZEEvK1h5Z0NKcXZ1YUwwTGliTTFLcmxV\nZG51NzEKWWVraFNKakVQbnZPaXNYSWs0SVh5d29HSU93dGp4a0R2Tkl0UXZhTVZsZHI0L2tiNnV2\nYmdkV3dxNUV3QlpYcQpsb3cya3lKb3YzOFY0VWsySThrdVhwTGNucnB3NVRpbzJvb2lVRTI3YjB2\nSFpxQktPZWk5VW84OHFDcm4zRUt4CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0t\nLS0K" + }, + "access_token_lifetime": 43200000000000, + "id_token_lifetime": 43200000000000 +} diff --git a/internal/query/testdata/oidc_client_public.json b/internal/query/testdata/oidc_client_public.json new file mode 100644 index 0000000000..ba23c95351 --- /dev/null +++ b/internal/query/testdata/oidc_client_public.json @@ -0,0 +1,25 @@ +{ + "instance_id": "230690539048009730", + "app_id": "236646457053020162", + "client_id": "236646457053085698@tests", + "client_secret": null, + "redirect_uris": ["http://localhost:9999/auth/callback"], + "response_types": [0], + "grant_types": [0], + "application_type": 0, + "auth_method_type": 2, + "post_logout_redirect_uris": null, + "is_dev_mode": true, + "access_token_type": 0, + "access_token_role_assertion": false, + "id_token_role_assertion": false, + "id_token_userinfo_assertion": false, + "clock_skew": 0, + "additional_origins": null, + "project_id": "236645808328409090", + "state": 1, + "project_role_keys": ["role1", "role2"], + "public_keys": null, + "access_token_lifetime": 43200000000000, + "id_token_lifetime": 43200000000000 +} diff --git a/internal/query/testdata/oidc_client_secret.json b/internal/query/testdata/oidc_client_secret.json new file mode 100644 index 0000000000..43b3256bb6 --- /dev/null +++ b/internal/query/testdata/oidc_client_secret.json @@ -0,0 +1,30 @@ +{ + "instance_id": "230690539048009730", + "app_id": "236646858984783874", + "client_id": "236646858984849410@tests", + "client_secret": { + "KeyID": "", + "Crypted": "JDJhJDE0JE96WjBYRVpaRXREMTNweS9FUGJhMmV2c1M2V2NLWjVvclZNajlwV0hFR0VIbUx1MmgzUEZx", + "Algorithm": "bcrypt", + "CryptoType": 1 + }, + "redirect_uris": ["http://localhost:9999/auth/callback"], + "response_types": [0], + "grant_types": [0], + "application_type": 0, + "auth_method_type": 0, + "post_logout_redirect_uris": null, + "is_dev_mode": true, + "access_token_type": 0, + "access_token_role_assertion": false, + "id_token_role_assertion": false, + "id_token_userinfo_assertion": false, + "clock_skew": 0, + "additional_origins": null, + "project_id": "236645808328409090", + "state": 1, + "project_role_keys": ["role1", "role2"], + "public_keys": null, + "access_token_lifetime": 43200000000000, + "id_token_lifetime": 43200000000000 +} diff --git a/internal/query/user.go b/internal/query/user.go index 144efec1dc..53a1647228 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" @@ -91,7 +92,7 @@ type Phone struct { type Machine struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` - HasSecret bool `json:"has_secret,omitempty"` + Secret *crypto.CryptoValue `json:"secret,omitempty"` AccessTokenType domain.OIDCTokenType `json:"access_token_type,omitempty"` } @@ -270,8 +271,8 @@ var ( name: projection.MachineDescriptionCol, table: machineTable, } - MachineHasSecretCol = Column{ - name: projection.MachineHasSecretCol, + MachineSecretCol = Column{ + name: projection.MachineSecretCol, table: machineTable, } MachineAccessTokenTypeCol = Column{ @@ -740,7 +741,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder MachineUserIDCol.identifier(), MachineNameCol.identifier(), MachineDescriptionCol.identifier(), - MachineHasSecretCol.identifier(), + MachineSecretCol.identifier(), MachineAccessTokenTypeCol.identifier(), countColumn.identifier(), ). @@ -777,7 +778,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder machineID := sql.NullString{} name := sql.NullString{} description := sql.NullString{} - hasSecret := sql.NullBool{} + var secret *crypto.CryptoValue accessTokenType := sql.NullInt32{} err := row.Scan( @@ -806,7 +807,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder &machineID, &name, &description, - &hasSecret, + &secret, &accessTokenType, &count, ) @@ -838,7 +839,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder u.Machine = &Machine{ Name: name.String, Description: description.String, - HasSecret: hasSecret.Bool, + Secret: secret, AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32), } } @@ -1210,7 +1211,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde MachineUserIDCol.identifier(), MachineNameCol.identifier(), MachineDescriptionCol.identifier(), - MachineHasSecretCol.identifier(), + MachineSecretCol.identifier(), MachineAccessTokenTypeCol.identifier(), countColumn.identifier()). From(userTable.identifier()). @@ -1249,7 +1250,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde machineID := sql.NullString{} name := sql.NullString{} description := sql.NullString{} - hasSecret := sql.NullBool{} + secret := new(crypto.CryptoValue) accessTokenType := sql.NullInt32{} err := rows.Scan( @@ -1278,7 +1279,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde &machineID, &name, &description, - &hasSecret, + secret, &accessTokenType, &count, ) @@ -1309,7 +1310,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde u.Machine = &Machine{ Name: name.String, Description: description.String, - HasSecret: hasSecret.Bool, + Secret: secret, AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32), } } diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index a9aae1d1c5..c78bf85699 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -39,38 +39,38 @@ var ( "method_type", "count", } - prepareActiveAuthMethodTypesStmt = `SELECT projections.users9_notifications.password_set,` + + prepareActiveAuthMethodTypesStmt = `SELECT projections.users10_notifications.password_set,` + ` auth_method_types.method_type,` + ` user_idps_count.count` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_notifications ON projections.users9.id = projections.users9_notifications.user_id AND projections.users9.instance_id = projections.users9_notifications.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_notifications ON projections.users10.id = projections.users10_notifications.user_id AND projections.users10.instance_id = projections.users10_notifications.instance_id` + ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` + ` WHERE auth_method_types.state = $1) AS auth_method_types` + - ` ON auth_method_types.user_id = projections.users9.id AND auth_method_types.instance_id = projections.users9.instance_id` + + ` ON auth_method_types.user_id = projections.users10.id AND auth_method_types.instance_id = projections.users10.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users9.id AND user_idps_count.instance_id = projections.users9.instance_id` + + ` ON user_idps_count.user_id = projections.users10.id AND user_idps_count.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms` prepareActiveAuthMethodTypesCols = []string{ "password_set", "method_type", "idps_count", } - prepareAuthMethodTypesRequiredStmt = `SELECT projections.users9_notifications.password_set,` + + prepareAuthMethodTypesRequiredStmt = `SELECT projections.users10_notifications.password_set,` + ` auth_method_types.method_type,` + ` user_idps_count.count,` + ` auth_methods_force_mfa.force_mfa,` + ` auth_methods_force_mfa.force_mfa_local_only` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_notifications ON projections.users9.id = projections.users9_notifications.user_id AND projections.users9.instance_id = projections.users9_notifications.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_notifications ON projections.users10.id = projections.users10_notifications.user_id AND projections.users10.instance_id = projections.users10_notifications.instance_id` + ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` + ` WHERE auth_method_types.state = $1) AS auth_method_types` + - ` ON auth_method_types.user_id = projections.users9.id AND auth_method_types.instance_id = projections.users9.instance_id` + + ` ON auth_method_types.user_id = projections.users10.id AND auth_method_types.instance_id = projections.users10.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users9.id AND user_idps_count.instance_id = projections.users9.instance_id` + + ` ON user_idps_count.user_id = projections.users10.id AND user_idps_count.instance_id = projections.users10.instance_id` + ` LEFT JOIN (SELECT auth_methods_force_mfa.force_mfa, auth_methods_force_mfa.force_mfa_local_only, auth_methods_force_mfa.instance_id, auth_methods_force_mfa.aggregate_id FROM projections.login_policies5 AS auth_methods_force_mfa ORDER BY auth_methods_force_mfa.is_default) AS auth_methods_force_mfa` + - ` ON (auth_methods_force_mfa.aggregate_id = projections.users9.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users9.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users9.instance_id` + + ` ON (auth_methods_force_mfa.aggregate_id = projections.users10.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users10.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms ` prepareAuthMethodTypesRequiredCols = []string{ diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 962c6a1ed6..3fe6aca131 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -23,14 +23,14 @@ var ( ", projections.user_grants3.roles" + ", projections.user_grants3.state" + ", projections.user_grants3.user_id" + - ", projections.users9.username" + - ", projections.users9.type" + - ", projections.users9.resource_owner" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.display_name" + - ", projections.users9_humans.avatar_key" + + ", projections.users10.username" + + ", projections.users10.type" + + ", projections.users10.resource_owner" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.email" + + ", projections.users10_humans.display_name" + + ", projections.users10_humans.avatar_key" + ", projections.login_names3.login_name" + ", projections.user_grants3.resource_owner" + ", projections.orgs1.name" + @@ -38,8 +38,8 @@ var ( ", projections.user_grants3.project_id" + ", projections.projects4.name" + " FROM projections.user_grants3" + - " LEFT JOIN projections.users9 ON projections.user_grants3.user_id = projections.users9.id AND projections.user_grants3.instance_id = projections.users9.instance_id" + - " LEFT JOIN projections.users9_humans ON projections.user_grants3.user_id = projections.users9_humans.user_id AND projections.user_grants3.instance_id = projections.users9_humans.instance_id" + + " LEFT JOIN projections.users10 ON projections.user_grants3.user_id = projections.users10.id AND projections.user_grants3.instance_id = projections.users10.instance_id" + + " LEFT JOIN projections.users10_humans ON projections.user_grants3.user_id = projections.users10_humans.user_id AND projections.user_grants3.instance_id = projections.users10_humans.instance_id" + " LEFT JOIN projections.orgs1 ON projections.user_grants3.resource_owner = projections.orgs1.id AND projections.user_grants3.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.projects4 ON projections.user_grants3.project_id = projections.projects4.id AND projections.user_grants3.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants3.user_id = projections.login_names3.user_id AND projections.user_grants3.instance_id = projections.login_names3.instance_id" + @@ -78,14 +78,14 @@ var ( ", projections.user_grants3.roles" + ", projections.user_grants3.state" + ", projections.user_grants3.user_id" + - ", projections.users9.username" + - ", projections.users9.type" + - ", projections.users9.resource_owner" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.display_name" + - ", projections.users9_humans.avatar_key" + + ", projections.users10.username" + + ", projections.users10.type" + + ", projections.users10.resource_owner" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.email" + + ", projections.users10_humans.display_name" + + ", projections.users10_humans.avatar_key" + ", projections.login_names3.login_name" + ", projections.user_grants3.resource_owner" + ", projections.orgs1.name" + @@ -94,8 +94,8 @@ var ( ", projections.projects4.name" + ", COUNT(*) OVER ()" + " FROM projections.user_grants3" + - " LEFT JOIN projections.users9 ON projections.user_grants3.user_id = projections.users9.id AND projections.user_grants3.instance_id = projections.users9.instance_id" + - " LEFT JOIN projections.users9_humans ON projections.user_grants3.user_id = projections.users9_humans.user_id AND projections.user_grants3.instance_id = projections.users9_humans.instance_id" + + " LEFT JOIN projections.users10 ON projections.user_grants3.user_id = projections.users10.id AND projections.user_grants3.instance_id = projections.users10.instance_id" + + " LEFT JOIN projections.users10_humans ON projections.user_grants3.user_id = projections.users10_humans.user_id AND projections.user_grants3.instance_id = projections.users10_humans.instance_id" + " LEFT JOIN projections.orgs1 ON projections.user_grants3.resource_owner = projections.orgs1.id AND projections.user_grants3.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.projects4 ON projections.user_grants3.project_id = projections.projects4.id AND projections.user_grants3.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants3.user_id = projections.login_names3.user_id AND projections.user_grants3.instance_id = projections.login_names3.instance_id" + diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 1076dd8729..3459c45dba 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -10,6 +10,7 @@ import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" errs "github.com/zitadel/zitadel/internal/errors" @@ -22,43 +23,43 @@ var ( preferredLoginNameQuery = `SELECT preferred_login_name.user_id, preferred_login_name.login_name, preferred_login_name.instance_id` + ` FROM projections.login_names3 AS preferred_login_name` + ` WHERE preferred_login_name.is_primary = $1` - userQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9.state,` + - ` projections.users9.type,` + - ` projections.users9.username,` + + userQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10.state,` + + ` projections.users10.type,` + + ` projections.users10.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.first_name,` + - ` projections.users9_humans.last_name,` + - ` projections.users9_humans.nick_name,` + - ` projections.users9_humans.display_name,` + - ` projections.users9_humans.preferred_language,` + - ` projections.users9_humans.gender,` + - ` projections.users9_humans.avatar_key,` + - ` projections.users9_humans.email,` + - ` projections.users9_humans.is_email_verified,` + - ` projections.users9_humans.phone,` + - ` projections.users9_humans.is_phone_verified,` + - ` projections.users9_machines.user_id,` + - ` projections.users9_machines.name,` + - ` projections.users9_machines.description,` + - ` projections.users9_machines.has_secret,` + - ` projections.users9_machines.access_token_type,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.first_name,` + + ` projections.users10_humans.last_name,` + + ` projections.users10_humans.nick_name,` + + ` projections.users10_humans.display_name,` + + ` projections.users10_humans.preferred_language,` + + ` projections.users10_humans.gender,` + + ` projections.users10_humans.avatar_key,` + + ` projections.users10_humans.email,` + + ` projections.users10_humans.is_email_verified,` + + ` projections.users10_humans.phone,` + + ` projections.users10_humans.is_phone_verified,` + + ` projections.users10_machines.user_id,` + + ` projections.users10_machines.name,` + + ` projections.users10_machines.description,` + + ` projections.users10_machines.secret,` + + ` projections.users10_machines.access_token_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9_machines ON projections.users9.id = projections.users9_machines.user_id AND projections.users9.instance_id = projections.users9_machines.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10_machines ON projections.users10.id = projections.users10_machines.user_id AND projections.users10.instance_id = projections.users10_machines.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users9.id AND login_names.instance_id = projections.users9.instance_id` + + ` ON login_names.user_id = projections.users10.id AND login_names.instance_id = projections.users10.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users9.id AND preferred_login_name.instance_id = projections.users9.instance_id` + + ` ON preferred_login_name.user_id = projections.users10.id AND preferred_login_name.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` userCols = []string{ "id", @@ -71,7 +72,7 @@ var ( "username", "loginnames", "login_name", - //human + // human "user_id", "first_name", "last_name", @@ -84,29 +85,29 @@ var ( "is_email_verified", "phone", "is_phone_verified", - //machine + // machine "user_id", "name", "description", - "has_secret", + "secret", "access_token_type", "count", } - profileQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.first_name,` + - ` projections.users9_humans.last_name,` + - ` projections.users9_humans.nick_name,` + - ` projections.users9_humans.display_name,` + - ` projections.users9_humans.preferred_language,` + - ` projections.users9_humans.gender,` + - ` projections.users9_humans.avatar_key` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + + profileQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.first_name,` + + ` projections.users10_humans.last_name,` + + ` projections.users10_humans.nick_name,` + + ` projections.users10_humans.display_name,` + + ` projections.users10_humans.preferred_language,` + + ` projections.users10_humans.gender,` + + ` projections.users10_humans.avatar_key` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` profileCols = []string{ "id", @@ -123,16 +124,16 @@ var ( "gender", "avatar_key", } - emailQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.email,` + - ` projections.users9_humans.is_email_verified` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + + emailQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.email,` + + ` projections.users10_humans.is_email_verified` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` emailCols = []string{ "id", @@ -144,16 +145,16 @@ var ( "email", "is_email_verified", } - phoneQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.phone,` + - ` projections.users9_humans.is_phone_verified` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + + phoneQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.phone,` + + ` projections.users10_humans.is_phone_verified` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` phoneCols = []string{ "id", @@ -165,14 +166,14 @@ var ( "phone", "is_phone_verified", } - userUniqueQuery = `SELECT projections.users9.id,` + - ` projections.users9.state,` + - ` projections.users9.username,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.email,` + - ` projections.users9_humans.is_email_verified` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + + userUniqueQuery = `SELECT projections.users10.id,` + + ` projections.users10.state,` + + ` projections.users10.username,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.email,` + + ` projections.users10_humans.is_email_verified` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` userUniqueCols = []string{ "id", @@ -182,40 +183,40 @@ var ( "email", "is_email_verified", } - notifyUserQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9.state,` + - ` projections.users9.type,` + - ` projections.users9.username,` + + notifyUserQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10.state,` + + ` projections.users10.type,` + + ` projections.users10.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.first_name,` + - ` projections.users9_humans.last_name,` + - ` projections.users9_humans.nick_name,` + - ` projections.users9_humans.display_name,` + - ` projections.users9_humans.preferred_language,` + - ` projections.users9_humans.gender,` + - ` projections.users9_humans.avatar_key,` + - ` projections.users9_notifications.user_id,` + - ` projections.users9_notifications.last_email,` + - ` projections.users9_notifications.verified_email,` + - ` projections.users9_notifications.last_phone,` + - ` projections.users9_notifications.verified_phone,` + - ` projections.users9_notifications.password_set,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.first_name,` + + ` projections.users10_humans.last_name,` + + ` projections.users10_humans.nick_name,` + + ` projections.users10_humans.display_name,` + + ` projections.users10_humans.preferred_language,` + + ` projections.users10_humans.gender,` + + ` projections.users10_humans.avatar_key,` + + ` projections.users10_notifications.user_id,` + + ` projections.users10_notifications.last_email,` + + ` projections.users10_notifications.verified_email,` + + ` projections.users10_notifications.last_phone,` + + ` projections.users10_notifications.verified_phone,` + + ` projections.users10_notifications.password_set,` + ` COUNT(*) OVER ()` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9_notifications ON projections.users9.id = projections.users9_notifications.user_id AND projections.users9.instance_id = projections.users9_notifications.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10_notifications ON projections.users10.id = projections.users10_notifications.user_id AND projections.users10.instance_id = projections.users10_notifications.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users9.id AND login_names.instance_id = projections.users9.instance_id` + + ` ON login_names.user_id = projections.users10.id AND login_names.instance_id = projections.users10.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users9.id AND preferred_login_name.instance_id = projections.users9.instance_id` + + ` ON preferred_login_name.user_id = projections.users10.id AND preferred_login_name.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` notifyUserCols = []string{ "id", @@ -228,7 +229,7 @@ var ( "username", "loginnames", "login_name", - //human + // human "user_id", "first_name", "last_name", @@ -237,7 +238,7 @@ var ( "preferred_language", "gender", "avatar_key", - //machine + // machine "user_id", "last_email", "verified_email", @@ -246,43 +247,43 @@ var ( "password_set", "count", } - usersQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9.state,` + - ` projections.users9.type,` + - ` projections.users9.username,` + + usersQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10.state,` + + ` projections.users10.type,` + + ` projections.users10.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.first_name,` + - ` projections.users9_humans.last_name,` + - ` projections.users9_humans.nick_name,` + - ` projections.users9_humans.display_name,` + - ` projections.users9_humans.preferred_language,` + - ` projections.users9_humans.gender,` + - ` projections.users9_humans.avatar_key,` + - ` projections.users9_humans.email,` + - ` projections.users9_humans.is_email_verified,` + - ` projections.users9_humans.phone,` + - ` projections.users9_humans.is_phone_verified,` + - ` projections.users9_machines.user_id,` + - ` projections.users9_machines.name,` + - ` projections.users9_machines.description,` + - ` projections.users9_machines.has_secret,` + - ` projections.users9_machines.access_token_type,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.first_name,` + + ` projections.users10_humans.last_name,` + + ` projections.users10_humans.nick_name,` + + ` projections.users10_humans.display_name,` + + ` projections.users10_humans.preferred_language,` + + ` projections.users10_humans.gender,` + + ` projections.users10_humans.avatar_key,` + + ` projections.users10_humans.email,` + + ` projections.users10_humans.is_email_verified,` + + ` projections.users10_humans.phone,` + + ` projections.users10_humans.is_phone_verified,` + + ` projections.users10_machines.user_id,` + + ` projections.users10_machines.name,` + + ` projections.users10_machines.description,` + + ` projections.users10_machines.secret,` + + ` projections.users10_machines.access_token_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9_machines ON projections.users9.id = projections.users9_machines.user_id AND projections.users9.instance_id = projections.users9_machines.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10_machines ON projections.users10.id = projections.users10_machines.user_id AND projections.users10.instance_id = projections.users10_machines.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users9.id AND login_names.instance_id = projections.users9.instance_id` + + ` ON login_names.user_id = projections.users10.id AND login_names.instance_id = projections.users10.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users9.id AND preferred_login_name.instance_id = projections.users9.instance_id` + + ` ON preferred_login_name.user_id = projections.users10.id AND preferred_login_name.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` usersCols = []string{ "id", @@ -295,7 +296,7 @@ var ( "username", "loginnames", "login_name", - //human + // human "user_id", "first_name", "last_name", @@ -308,11 +309,11 @@ var ( "is_email_verified", "phone", "is_phone_verified", - //machine + // machine "user_id", "name", "description", - "has_secret", + "secret", "access_token_type", "count", } @@ -365,7 +366,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -378,7 +379,7 @@ func Test_UserPrepares(t *testing.T) { true, "phone", true, - //machine + // machine nil, nil, nil, @@ -432,7 +433,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human nil, nil, nil, @@ -445,11 +446,11 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, - //machine + // machine "id", "name", "description", - true, + nil, domain.OIDCTokenTypeBearer, 1, }, @@ -469,7 +470,71 @@ func Test_UserPrepares(t *testing.T) { Machine: &Machine{ Name: "name", Description: "description", - HasSecret: true, + Secret: nil, + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + }, + }, + { + name: "prepareUserQuery machine with secret found", + prepare: prepareUserQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userQuery), + userCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeMachine, + "username", + database.TextArray[string]{"login_name1", "login_name2"}, + "login_name1", + // human + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // machine + "id", + "name", + "description", + `{"CryptoType":1,"Algorithm":"bcrypt","Crypted":"deadbeef"}`, + domain.OIDCTokenTypeBearer, + 1, + }, + ), + }, + object: &User{ + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + State: domain.UserStateActive, + Type: domain.UserTypeMachine, + Username: "username", + LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, + PreferredLoginName: "login_name1", + Machine: &Machine{ + Name: "name", + Description: "description", + Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "bcrypt", + Crypted: []byte{117, 230, 157, 109, 231, 159}, + }, AccessTokenType: domain.OIDCTokenTypeBearer, }, }, @@ -875,7 +940,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -938,7 +1003,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -1019,7 +1084,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -1032,7 +1097,7 @@ func Test_UserPrepares(t *testing.T) { true, "phone", true, - //machine + // machine nil, nil, nil, @@ -1094,7 +1159,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -1107,7 +1172,7 @@ func Test_UserPrepares(t *testing.T) { true, "phone", true, - //machine + // machine nil, nil, nil, @@ -1125,7 +1190,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human nil, nil, nil, @@ -1138,11 +1203,11 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, - //machine + // machine "id", "name", "description", - true, + `{"CryptoType":1,"Algorithm":"bcrypt","Crypted":"deadbeef"}`, domain.OIDCTokenTypeBearer, }, }, @@ -1190,9 +1255,13 @@ func Test_UserPrepares(t *testing.T) { LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, PreferredLoginName: "login_name1", Machine: &Machine{ - Name: "name", - Description: "description", - HasSecret: true, + Name: "name", + Description: "description", + Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "bcrypt", + Crypted: []byte{117, 230, 157, 109, 231, 159}, + }, AccessTokenType: domain.OIDCTokenTypeBearer, }, }, diff --git a/internal/query/userinfo.go b/internal/query/userinfo.go index c42a893319..9ee2721f54 100644 --- a/internal/query/userinfo.go +++ b/internal/query/userinfo.go @@ -4,12 +4,12 @@ import ( "context" "database/sql" _ "embed" - "encoding/json" + "errors" "sync" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/errors" + zerrors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -40,23 +40,17 @@ func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string, roleAudien ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - var data []byte - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - return row.Scan(&data) - }, - oidcUserInfoQuery, + userInfo, err := database.QueryJSONObject[OIDCUserInfo](ctx, q.client, oidcUserInfoQuery, userID, authz.GetInstance(ctx).InstanceID(), database.TextArray[string](roleAudience), ) - if err != nil { - return nil, errors.ThrowInternal(err, "QUERY-Oath6", "Errors.Internal") + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-Eey2a", "Errors.User.NotFound") } - - userInfo := new(OIDCUserInfo) - if err = json.Unmarshal(data, userInfo); err != nil { - return nil, errors.ThrowInternal(err, "QUERY-Vohs6", "Errors.Internal") + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Oath6", "Errors.Internal") } if userInfo.User == nil { - return nil, errors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound") + return nil, zerrors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound") } return userInfo, nil diff --git a/internal/query/userinfo_test.go b/internal/query/userinfo_test.go index 34c713d506..7629247605 100644 --- a/internal/query/userinfo_test.go +++ b/internal/query/userinfo_test.go @@ -68,14 +68,6 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) { mock: mockQueryErr(expQuery, sql.ErrConnDone, "231965491734773762", "instanceID", nil), wantErr: sql.ErrConnDone, }, - { - name: "unmarshal error", - args: args{ - userID: "231965491734773762", - }, - mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{`~~~`}, "231965491734773762", "instanceID", nil), - wantErr: errors.ThrowInternal(nil, "QUERY-Vohs6", "Errors.Internal"), - }, { name: "user not found", args: args{