From 3473156c7e2530b4fa5130f3c13f62056300ecd5 Mon Sep 17 00:00:00 2001 From: Silvan Date: Fri, 26 Nov 2021 07:57:05 +0100 Subject: [PATCH] fix(app): move queries to query package (#2612) * fix: move queries to query package * fix(auth): switch project role requests to query pkg * refactor: delete unused project role code * remove repo * implement sql queries * fix(database): oidc config change type to int2 * fix(queries): implement app queries * refactor: simplify code * fix: correct app query * Update app.go * fix token check * fix mock * test: app prepares * test: oidc compliance * test: OIDCOriginAllowList * fix: converter * resolve unsupported oidc version Co-authored-by: Livio Amstutz --- internal/api/api.go | 4 +- internal/api/authz/permissions_test.go | 6 +- internal/api/authz/token.go | 27 +- .../grpc/management/project_application.go | 22 +- .../project_application_converter.go | 24 +- internal/api/grpc/project/application.go | 85 +- .../middleware/auth_interceptor_test.go | 6 +- internal/api/oidc/auth_request.go | 16 +- internal/api/oidc/client.go | 27 +- internal/api/oidc/client_converter.go | 73 +- internal/auth/repository/application.go | 3 - .../eventsourcing/eventstore/application.go | 18 +- .../eventsourcing/eventstore/auth_request.go | 35 +- .../eventstore/auth_request_test.go | 8 +- .../eventsourcing/handler/application.go | 168 -- .../eventsourcing/handler/handler.go | 1 - .../repository/eventsourcing/repository.go | 17 +- .../eventsourcing/view/application.go | 133 -- .../repository/eventsourcing/view/view.go | 5 +- .../eventstore/token_verifier.go | 21 +- .../eventsourcing/handler/application.go | 114 -- .../eventsourcing/handler/handler.go | 2 - .../repository/eventsourcing/repository.go | 4 +- .../eventsourcing/view/application.go | 80 +- .../repository/eventsourcing/view/view.go | 6 +- internal/domain/application_oidc.go | 57 +- internal/domain/application_oidc_test.go | 488 +++++ .../eventsourcing/eventstore/project.go | 57 - .../eventsourcing/handler/application.go | 167 -- .../eventsourcing/handler/handler.go | 1 - .../eventsourcing/view/application.go | 74 - internal/management/repository/project.go | 2 - .../repository/view/application_view.go | 89 - internal/query/app.go | 658 +++++++ internal/query/app_test.go | 1699 +++++++++++++++++ internal/query/project_test.go | 16 + internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/it.yaml | 1 + 39 files changed, 3150 insertions(+), 1066 deletions(-) delete mode 100644 internal/auth/repository/eventsourcing/handler/application.go delete mode 100644 internal/auth/repository/eventsourcing/view/application.go delete mode 100644 internal/authz/repository/eventsourcing/handler/application.go delete mode 100644 internal/management/repository/eventsourcing/handler/application.go delete mode 100644 internal/management/repository/eventsourcing/view/application.go delete mode 100644 internal/project/repository/view/application_view.go create mode 100644 internal/query/app.go create mode 100644 internal/query/app_test.go diff --git a/internal/api/api.go b/internal/api/api.go index 630846a92..754269619 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -47,7 +47,7 @@ type API struct { type health interface { Health(ctx context.Context) error IamByID(ctx context.Context) (*iam_model.IAM, error) - VerifierClientID(ctx context.Context, appName string) (string, error) + VerifierClientID(ctx context.Context, appName string) (string, string, error) } type auth interface { @@ -159,7 +159,7 @@ func handleValidate(checks []ValidationFunction) func(w http.ResponseWriter, r * } func (a *API) handleClientID(w http.ResponseWriter, r *http.Request) { - id, err := a.health.VerifierClientID(r.Context(), "Zitadel Console") + id, _, err := a.health.VerifierClientID(r.Context(), "Zitadel Console") if err != nil { http_util.MarshalJSON(w, nil, err, http.StatusPreconditionFailed) return diff --git a/internal/api/authz/permissions_test.go b/internal/api/authz/permissions_test.go index be25ac77c..2a3eeddb9 100644 --- a/internal/api/authz/permissions_test.go +++ b/internal/api/authz/permissions_test.go @@ -15,7 +15,7 @@ type testVerifier struct { memberships []*Membership } -func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, string, string, string, error) { +func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { return "userID", "agentID", "clientID", "de", "orgID", nil } func (v *testVerifier) SearchMyMemberships(ctx context.Context) ([]*Membership, error) { @@ -30,8 +30,8 @@ func (v *testVerifier) ExistsOrg(ctx context.Context, orgID string) error { return nil } -func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, error) { - return "clientID", nil +func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, string, error) { + return "clientID", "projectID", nil } func (v *testVerifier) CheckOrgFeatures(context.Context, string, ...string) error { diff --git a/internal/api/authz/token.go b/internal/api/authz/token.go index 58a7fd046..93c46b7c4 100644 --- a/internal/api/authz/token.go +++ b/internal/api/authz/token.go @@ -20,8 +20,8 @@ type TokenVerifier struct { } type authZRepo interface { - VerifyAccessToken(ctx context.Context, token, verifierClientID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error) - VerifierClientID(ctx context.Context, name string) (clientID string, err error) + VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error) + VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error) SearchMyMemberships(ctx context.Context) ([]*Membership, error) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) ExistsOrg(ctx context.Context, orgID string) error @@ -33,17 +33,18 @@ func Start(authZRepo authZRepo) (v *TokenVerifier) { } func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string, method string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) { - verifierClientID, err := v.clientIDFromMethod(ctx, method) + verifierClientID, projectID, err := v.clientIDAndProjectIDFromMethod(ctx, method) if err != nil { return "", "", "", "", "", err } - userID, agentID, clientID, prefLang, resourceOwner, err = v.authZRepo.VerifyAccessToken(ctx, token, verifierClientID) + userID, agentID, clientID, prefLang, resourceOwner, err = v.authZRepo.VerifyAccessToken(ctx, token, verifierClientID, projectID) return userID, clientID, agentID, prefLang, resourceOwner, err } type client struct { - id string - name string + id string + projectID string + name string } func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) { @@ -64,28 +65,28 @@ func prefixFromMethod(method string) (string, bool) { return parts[1], true } -func (v *TokenVerifier) clientIDFromMethod(ctx context.Context, method string) (_ string, err error) { +func (v *TokenVerifier) clientIDAndProjectIDFromMethod(ctx context.Context, method string) (clientID, projectID string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() prefix, ok := prefixFromMethod(method) if !ok { - return "", caos_errs.ThrowPermissionDenied(nil, "AUTHZ-GRD2Q", "Errors.Internal") + return "", "", caos_errs.ThrowPermissionDenied(nil, "AUTHZ-GRD2Q", "Errors.Internal") } app, ok := v.clients.Load(prefix) if !ok { - return "", caos_errs.ThrowPermissionDenied(nil, "AUTHZ-G2qrh", "Errors.Internal") + return "", "", caos_errs.ThrowPermissionDenied(nil, "AUTHZ-G2qrh", "Errors.Internal") } c := app.(*client) if c.id != "" { - return c.id, nil + return c.id, c.projectID, nil } - c.id, err = v.authZRepo.VerifierClientID(ctx, c.name) + c.id, c.projectID, err = v.authZRepo.VerifierClientID(ctx, c.name) if err != nil { - return "", caos_errs.ThrowPermissionDenied(err, "AUTHZ-ptTIF2", "Errors.Internal") + return "", "", caos_errs.ThrowPermissionDenied(err, "AUTHZ-ptTIF2", "Errors.Internal") } v.clients.Store(prefix, c) - return c.id, nil + return c.id, c.projectID, nil } func (v *TokenVerifier) SearchMyMemberships(ctx context.Context) (_ []*Membership, err error) { ctx, span := tracing.NewSpan(ctx) diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index 1c9cb1d93..7f605442a 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -12,7 +12,7 @@ import ( ) func (s *Server) GetAppByID(ctx context.Context, req *mgmt_pb.GetAppByIDRequest) (*mgmt_pb.GetAppByIDResponse, error) { - app, err := s.project.ApplicationByID(ctx, req.ProjectId, req.AppId) + app, err := s.query.AppByProjectAndAppID(ctx, req.ProjectId, req.AppId) if err != nil { return nil, err } @@ -26,16 +26,16 @@ func (s *Server) ListApps(ctx context.Context, req *mgmt_pb.ListAppsRequest) (*m if err != nil { return nil, err } - domains, err := s.project.SearchApplications(ctx, queries) + apps, err := s.query.SearchApps(ctx, queries) if err != nil { return nil, err } return &mgmt_pb.ListAppsResponse{ - Result: project_grpc.AppsToPb(domains.Result), + Result: project_grpc.AppsToPb(apps.Apps), Details: object_grpc.ToListDetails( - domains.TotalResult, - domains.Sequence, - domains.Timestamp, + apps.Count, + apps.Sequence, + apps.Timestamp, ), }, nil } @@ -196,16 +196,16 @@ func (s *Server) ListAppKeys(ctx context.Context, req *mgmt_pb.ListAppKeysReques if err != nil { return nil, err } - domains, err := s.project.SearchClientKeys(ctx, queries) + keys, err := s.project.SearchClientKeys(ctx, queries) if err != nil { return nil, err } return &mgmt_pb.ListAppKeysResponse{ - Result: authn_grpc.KeyViewsToPb(domains.Result), + Result: authn_grpc.KeyViewsToPb(keys.Result), Details: object_grpc.ToListDetails( - domains.TotalResult, - domains.Sequence, - domains.Timestamp, + keys.TotalResult, + keys.Sequence, + keys.Timestamp, ), }, nil } diff --git a/internal/api/grpc/management/project_application_converter.go b/internal/api/grpc/management/project_application_converter.go index 516bae58e..bda8e5ede 100644 --- a/internal/api/grpc/management/project_application_converter.go +++ b/internal/api/grpc/management/project_application_converter.go @@ -9,25 +9,27 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore/v1/models" key_model "github.com/caos/zitadel/internal/key/model" - proj_model "github.com/caos/zitadel/internal/project/model" + "github.com/caos/zitadel/internal/query" mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" ) -func ListAppsRequestToModel(req *mgmt_pb.ListAppsRequest) (*proj_model.ApplicationSearchRequest, error) { +func ListAppsRequestToModel(req *mgmt_pb.ListAppsRequest) (*query.AppSearchQueries, error) { offset, limit, asc := object.ListQueryToModel(req.Query) queries, err := app_grpc.AppQueriesToModel(req.Queries) if err != nil { return nil, err } - queries = append(queries, &proj_model.ApplicationSearchQuery{ - Key: proj_model.AppSearchKeyProjectID, - Method: domain.SearchMethodEquals, - Value: req.ProjectId, - }) - return &proj_model.ApplicationSearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, + projectQuery, err := query.NewAppProjectIDSearchQuery(req.ProjectId) + if err != nil { + return nil, err + } + queries = append(queries, projectQuery) + return &query.AppSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, //SortingColumn: //TODO: sorting Queries: queries, }, nil diff --git a/internal/api/grpc/project/application.go b/internal/api/grpc/project/application.go index f72ce352b..85a3f7bb1 100644 --- a/internal/api/grpc/project/application.go +++ b/internal/api/grpc/project/application.go @@ -7,11 +7,12 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" proj_model "github.com/caos/zitadel/internal/project/model" + "github.com/caos/zitadel/internal/query" app_pb "github.com/caos/zitadel/pkg/grpc/app" message_pb "github.com/caos/zitadel/pkg/grpc/message" ) -func AppsToPb(apps []*proj_model.ApplicationView) []*app_pb.App { +func AppsToPb(apps []*query.App) []*app_pb.App { a := make([]*app_pb.App, len(apps)) for i, app := range apps { a[i] = AppToPb(app) @@ -19,7 +20,7 @@ func AppsToPb(apps []*proj_model.ApplicationView) []*app_pb.App { return a } -func AppToPb(app *proj_model.ApplicationView) *app_pb.App { +func AppToPb(app *query.App) *app_pb.App { return &app_pb.App{ Id: app.ID, Details: object_grpc.ToViewDetailsPb(app.Sequence, app.CreationDate, app.ChangeDate, app.ResourceOwner), @@ -29,68 +30,68 @@ func AppToPb(app *proj_model.ApplicationView) *app_pb.App { } } -func AppConfigToPb(app *proj_model.ApplicationView) app_pb.AppConfig { - if app.IsOIDC { - return AppOIDCConfigToPb(app) +func AppConfigToPb(app *query.App) app_pb.AppConfig { + if app.OIDCConfig != nil { + return AppOIDCConfigToPb(app.OIDCConfig) } - return AppAPIConfigToPb(app) + return AppAPIConfigToPb(app.APIConfig) } -func AppOIDCConfigToPb(app *proj_model.ApplicationView) *app_pb.App_OidcConfig { +func AppOIDCConfigToPb(app *query.OIDCApp) *app_pb.App_OidcConfig { return &app_pb.App_OidcConfig{ OidcConfig: &app_pb.OIDCConfig{ - RedirectUris: app.OIDCRedirectUris, - ResponseTypes: OIDCResponseTypesFromModel(app.OIDCResponseTypes), - GrantTypes: OIDCGrantTypesFromModel(app.OIDCGrantTypes), - AppType: OIDCApplicationTypeToPb(domain.OIDCApplicationType(app.OIDCApplicationType)), - ClientId: app.OIDCClientID, - AuthMethodType: OIDCAuthMethodTypeToPb(domain.OIDCAuthMethodType(app.OIDCAuthMethodType)), - PostLogoutRedirectUris: app.OIDCPostLogoutRedirectUris, - Version: OIDCVersionToPb(domain.OIDCVersion(app.OIDCVersion)), - NoneCompliant: app.NoneCompliant, + RedirectUris: app.RedirectURIs, + ResponseTypes: OIDCResponseTypesFromModel(app.ResponseTypes), + GrantTypes: OIDCGrantTypesFromModel(app.GrantTypes), + AppType: OIDCApplicationTypeToPb(app.AppType), + ClientId: app.ClientID, + AuthMethodType: OIDCAuthMethodTypeToPb(app.AuthMethodType), + PostLogoutRedirectUris: app.PostLogoutRedirectURIs, + Version: OIDCVersionToPb(domain.OIDCVersion(app.Version)), + NoneCompliant: len(app.ComplianceProblems) != 0, ComplianceProblems: ComplianceProblemsToLocalizedMessages(app.ComplianceProblems), - DevMode: app.DevMode, - AccessTokenType: oidcTokenTypeToPb(domain.OIDCTokenType(app.AccessTokenType)), - AccessTokenRoleAssertion: app.AccessTokenRoleAssertion, - IdTokenRoleAssertion: app.IDTokenRoleAssertion, - IdTokenUserinfoAssertion: app.IDTokenUserinfoAssertion, + DevMode: app.IsDevMode, + AccessTokenType: oidcTokenTypeToPb(app.AccessTokenType), + AccessTokenRoleAssertion: app.AssertAccessTokenRole, + IdTokenRoleAssertion: app.AssertIDTokenRole, + IdTokenUserinfoAssertion: app.AssertIDTokenUserinfo, ClockSkew: durationpb.New(app.ClockSkew), AdditionalOrigins: app.AdditionalOrigins, - AllowedOrigins: app.OriginAllowList, + AllowedOrigins: app.AllowedOrigins, }, } } -func AppAPIConfigToPb(app *proj_model.ApplicationView) app_pb.AppConfig { +func AppAPIConfigToPb(app *query.APIApp) app_pb.AppConfig { return &app_pb.App_ApiConfig{ ApiConfig: &app_pb.APIConfig{ - ClientId: app.OIDCClientID, + ClientId: app.ClientID, ClientSecret: "", //TODO: remove from proto - AuthMethodType: APIAuthMethodeTypeToPb(domain.APIAuthMethodType(app.OIDCAuthMethodType)), + AuthMethodType: APIAuthMethodeTypeToPb(app.AuthMethodType), }, } } -func AppStateToPb(state proj_model.AppState) app_pb.AppState { +func AppStateToPb(state domain.AppState) app_pb.AppState { switch state { - case proj_model.AppStateActive: + case domain.AppStateActive: return app_pb.AppState_APP_STATE_ACTIVE - case proj_model.AppStateInactive: + case domain.AppStateInactive: return app_pb.AppState_APP_STATE_INACTIVE default: return app_pb.AppState_APP_STATE_UNSPECIFIED } } -func OIDCResponseTypesFromModel(responseTypes []proj_model.OIDCResponseType) []app_pb.OIDCResponseType { +func OIDCResponseTypesFromModel(responseTypes []domain.OIDCResponseType) []app_pb.OIDCResponseType { oidcResponseTypes := make([]app_pb.OIDCResponseType, len(responseTypes)) for i, responseType := range responseTypes { switch responseType { - case proj_model.OIDCResponseTypeCode: + case domain.OIDCResponseTypeCode: oidcResponseTypes[i] = app_pb.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE - case proj_model.OIDCResponseTypeIDToken: + case domain.OIDCResponseTypeIDToken: oidcResponseTypes[i] = app_pb.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN - case proj_model.OIDCResponseTypeIDTokenToken: + case domain.OIDCResponseTypeIDTokenToken: oidcResponseTypes[i] = app_pb.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN } } @@ -98,7 +99,7 @@ func OIDCResponseTypesFromModel(responseTypes []proj_model.OIDCResponseType) []a } func OIDCResponseTypesToDomain(responseTypes []app_pb.OIDCResponseType) []domain.OIDCResponseType { - if responseTypes == nil || len(responseTypes) == 0 { + if len(responseTypes) == 0 { return []domain.OIDCResponseType{domain.OIDCResponseTypeCode} } oidcResponseTypes := make([]domain.OIDCResponseType, len(responseTypes)) @@ -115,15 +116,15 @@ func OIDCResponseTypesToDomain(responseTypes []app_pb.OIDCResponseType) []domain return oidcResponseTypes } -func OIDCGrantTypesFromModel(grantTypes []proj_model.OIDCGrantType) []app_pb.OIDCGrantType { +func OIDCGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app_pb.OIDCGrantType { oidcGrantTypes := make([]app_pb.OIDCGrantType, len(grantTypes)) for i, grantType := range grantTypes { switch grantType { - case proj_model.OIDCGrantTypeAuthorizationCode: + case domain.OIDCGrantTypeAuthorizationCode: oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE - case proj_model.OIDCGrantTypeImplicit: + case domain.OIDCGrantTypeImplicit: oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT - case proj_model.OIDCGrantTypeRefreshToken: + case domain.OIDCGrantTypeRefreshToken: oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN } } @@ -272,8 +273,8 @@ func APIAuthMethodTypeToDomain(authType app_pb.APIAuthMethodType) domain.APIAuth } } -func AppQueriesToModel(queries []*app_pb.AppQuery) (_ []*proj_model.ApplicationSearchQuery, err error) { - q := make([]*proj_model.ApplicationSearchQuery, len(queries)) +func AppQueriesToModel(queries []*app_pb.AppQuery) (q []query.SearchQuery, err error) { + q = make([]query.SearchQuery, len(queries)) for i, query := range queries { q[i], err = AppQueryToModel(query) if err != nil { @@ -283,10 +284,10 @@ func AppQueriesToModel(queries []*app_pb.AppQuery) (_ []*proj_model.ApplicationS return q, nil } -func AppQueryToModel(query *app_pb.AppQuery) (*proj_model.ApplicationSearchQuery, error) { - switch q := query.Query.(type) { +func AppQueryToModel(appQuery *app_pb.AppQuery) (query.SearchQuery, error) { + switch q := appQuery.Query.(type) { case *app_pb.AppQuery_NameQuery: - return AppQueryNameToModel(q.NameQuery), nil + return query.NewAppNameSearchQuery(object_grpc.TextMethodToQuery(q.NameQuery.Method), q.NameQuery.Name) default: return nil, errors.ThrowInvalidArgument(nil, "APP-Add46", "List.Query.Invalid") } diff --git a/internal/api/grpc/server/middleware/auth_interceptor_test.go b/internal/api/grpc/server/middleware/auth_interceptor_test.go index 234b48048..4dcda1541 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor_test.go +++ b/internal/api/grpc/server/middleware/auth_interceptor_test.go @@ -21,7 +21,7 @@ var ( type verifierMock struct{} -func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, string, string, string, error) { +func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { return "", "", "", "", "", nil } func (v *verifierMock) SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) { @@ -34,8 +34,8 @@ func (v *verifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, client func (v *verifierMock) ExistsOrg(ctx context.Context, orgID string) error { return nil } -func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, error) { - return "", nil +func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) { + return "", "", nil } func (v *verifierMock) CheckOrgFeatures(context.Context, string, ...string) error { return nil diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 6739d0a1e..05c2e0bb8 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -13,7 +13,6 @@ import ( "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/errors" - proj_model "github.com/caos/zitadel/internal/project/model" "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/telemetry/tracing" grant_model "github.com/caos/zitadel/internal/usergrant/model" @@ -26,15 +25,20 @@ func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest if !ok { return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sd436", "no user agent id") } - app, err := o.repo.ApplicationByClientID(ctx, req.ClientID) + projectID, err := o.query.ProjectIDFromOIDCClientID(ctx, req.ClientID) if err != nil { return nil, errors.ThrowPreconditionFailed(nil, "OIDC-AEG4d", "Errors.Internal") } - req.Scopes, err = o.assertProjectRoleScopes(app, req.Scopes) + project, err := o.query.ProjectByID(ctx, projectID) + if err != nil { + return nil, errors.ThrowPreconditionFailed(nil, "OIDC-w4wIn", "Errors.Internal") + } + req.Scopes, err = o.assertProjectRoleScopes(project, req.Scopes) if err != nil { return nil, errors.ThrowPreconditionFailed(nil, "OIDC-Gqrfg", "Errors.Internal") } authRequest := CreateAuthRequestToBusiness(ctx, req, userAgentID, userID) + //TODO: ensure splitting of command and query side durring auth request and login refactoring resp, err := o.repo.CreateAuthRequest(ctx, authRequest) if err != nil { return nil, err @@ -205,8 +209,8 @@ func (o *OPStorage) GetKeySet(ctx context.Context) (_ *jose.JSONWebKeySet, err e return o.repo.GetKeySet(ctx) } -func (o *OPStorage) assertProjectRoleScopes(app *proj_model.ApplicationView, scopes []string) ([]string, error) { - if !app.ProjectRoleAssertion { +func (o *OPStorage) assertProjectRoleScopes(project *query.Project, scopes []string) ([]string, error) { + if !project.ProjectRoleAssertion { return scopes, nil } for _, scope := range scopes { @@ -214,7 +218,7 @@ func (o *OPStorage) assertProjectRoleScopes(app *proj_model.ApplicationView, sco return scopes, nil } } - projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(app.ProjectID) + projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(project.ID) if err != nil { return nil, errors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 920874298..4ba45baf8 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -12,11 +12,10 @@ import ( "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/http" - "github.com/caos/zitadel/internal/auth_request/model" authreq_model "github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" - proj_model "github.com/caos/zitadel/internal/project/model" "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/telemetry/tracing" user_model "github.com/caos/zitadel/internal/user/model" @@ -37,11 +36,11 @@ 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.repo.ApplicationByClientID(ctx, id) + client, err := o.query.AppByOIDCClientID(ctx, id) if err != nil { return nil, err } - if client.State != proj_model.AppStateActive { + if client.State != domain.AppStateActive { return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sdaGg", "client is not active") } projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(client.ProjectID) @@ -125,11 +124,11 @@ func (o *OPStorage) SetUserinfoFromToken(ctx context.Context, userInfo oidc.User return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired") } if token.ApplicationID != "" { - app, err := o.repo.ApplicationByClientID(ctx, token.ApplicationID) + app, err := o.query.AppByOIDCClientID(ctx, token.ApplicationID) if err != nil { return err } - if origin != "" && !http.IsOriginAllowed(app.OriginAllowList, origin) { + if origin != "" && !http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) { return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed") } } @@ -202,8 +201,8 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo oidc.Use if strings.HasPrefix(scope, ScopeProjectRolePrefix) { roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix)) } - if strings.HasPrefix(scope, model.OrgDomainPrimaryScope) { - userInfo.AppendClaims(model.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, model.OrgDomainPrimaryScope)) + if strings.HasPrefix(scope, authreq_model.OrgDomainPrimaryScope) { + userInfo.AppendClaims(authreq_model.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, authreq_model.OrgDomainPrimaryScope)) } } } @@ -225,12 +224,12 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection if err != nil { return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired") } - app, err := o.repo.ApplicationByClientID(ctx, clientID) + projectID, err := o.query.ProjectIDFromOIDCClientID(ctx, clientID) if err != nil { return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found") } for _, aud := range token.Audience { - if aud == clientID || aud == app.ProjectID { + if aud == clientID || aud == projectID { err := o.SetUserinfoFromScopes(ctx, introspection, token.UserID, clientID, token.Scopes) if err != nil { return err @@ -266,8 +265,8 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie } if strings.HasPrefix(scope, ScopeProjectRolePrefix) { roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix)) - } else if strings.HasPrefix(scope, model.OrgDomainPrimaryScope) { - claims = appendClaim(claims, model.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, model.OrgDomainPrimaryScope)) + } else if strings.HasPrefix(scope, authreq_model.OrgDomainPrimaryScope) { + claims = appendClaim(claims, authreq_model.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, authreq_model.OrgDomainPrimaryScope)) } } if len(roles) == 0 || clientID == "" { @@ -284,11 +283,11 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie } func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID string, requestedRoles []string) (map[string]map[string]string, error) { - app, err := o.repo.ApplicationByClientID(ctx, applicationID) + projectID, err := o.query.ProjectIDFromOIDCClientID(ctx, applicationID) if err != nil { return nil, err } - grants, err := o.repo.UserGrantsByProjectAndUserID(app.ProjectID, userID) + grants, err := o.repo.UserGrantsByProjectAndUserID(projectID, userID) if err != nil { return nil, err } diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index a285b8017..d1e7d461a 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -8,24 +8,25 @@ import ( "github.com/caos/oidc/pkg/op" authreq_model "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/project/model" + "github.com/caos/zitadel/internal/query" ) type Client struct { - *model.ApplicationView + app *query.App defaultLoginURL string defaultAccessTokenLifetime time.Duration defaultIdTokenLifetime time.Duration allowedScopes []string } -func ClientFromBusiness(app *model.ApplicationView, defaultLoginURL string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration, allowedScopes []string) (op.Client, error) { - if !app.IsOIDC { +func ClientFromBusiness(app *query.App, defaultLoginURL 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") } return &Client{ - ApplicationView: app, + app: app, defaultLoginURL: defaultLoginURL, defaultAccessTokenLifetime: defaultAccessTokenLifetime, defaultIdTokenLifetime: defaultIdTokenLifetime, @@ -34,15 +35,15 @@ func ClientFromBusiness(app *model.ApplicationView, defaultLoginURL string, defa } func (c *Client) ApplicationType() op.ApplicationType { - return op.ApplicationType(c.OIDCApplicationType) + return op.ApplicationType(c.app.OIDCConfig.AppType) } func (c *Client) AuthMethod() oidc.AuthMethod { - return authMethodToOIDC(c.OIDCAuthMethodType) + return authMethodToOIDC(c.app.OIDCConfig.AuthMethodType) } func (c *Client) GetID() string { - return c.OIDCClientID + return c.app.OIDCConfig.ClientID } func (c *Client) LoginURL(id string) string { @@ -50,28 +51,28 @@ func (c *Client) LoginURL(id string) string { } func (c *Client) RedirectURIs() []string { - return c.OIDCRedirectUris + return c.app.OIDCConfig.RedirectURIs } func (c *Client) PostLogoutRedirectURIs() []string { - return c.OIDCPostLogoutRedirectUris + return c.app.OIDCConfig.PostLogoutRedirectURIs } func (c *Client) ResponseTypes() []oidc.ResponseType { - return responseTypesToOIDC(c.OIDCResponseTypes) + return responseTypesToOIDC(c.app.OIDCConfig.ResponseTypes) } func (c *Client) GrantTypes() []oidc.GrantType { - return grantTypesToOIDC(c.OIDCGrantTypes) + return grantTypesToOIDC(c.app.OIDCConfig.GrantTypes) } func (c *Client) DevMode() bool { - return c.ApplicationView.DevMode + return c.app.OIDCConfig.IsDevMode } func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { - if c.IDTokenRoleAssertion { + if c.app.OIDCConfig.AssertIDTokenRole { return scopes } return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix) @@ -80,7 +81,7 @@ func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []strin func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { - if c.AccessTokenRoleAssertion { + if c.app.OIDCConfig.AssertAccessTokenRole { return scopes } return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix) @@ -96,7 +97,7 @@ func (c *Client) IDTokenLifetime() time.Duration { } func (c *Client) AccessTokenType() op.AccessTokenType { - return accessTokenTypeToOIDC(c.ApplicationView.AccessTokenType) + return accessTokenTypeToOIDC(c.app.OIDCConfig.AccessTokenType) } func (c *Client) IsScopeAllowed(scope string) bool { @@ -124,40 +125,40 @@ func (c *Client) IsScopeAllowed(scope string) bool { } func (c *Client) ClockSkew() time.Duration { - return c.ApplicationView.ClockSkew + return c.app.OIDCConfig.ClockSkew } func (c *Client) IDTokenUserinfoClaimsAssertion() bool { - return c.ApplicationView.IDTokenUserinfoAssertion + return c.app.OIDCConfig.AssertIDTokenUserinfo } -func accessTokenTypeToOIDC(tokenType model.OIDCTokenType) op.AccessTokenType { +func accessTokenTypeToOIDC(tokenType domain.OIDCTokenType) op.AccessTokenType { switch tokenType { - case model.OIDCTokenTypeBearer: + case domain.OIDCTokenTypeBearer: return op.AccessTokenTypeBearer - case model.OIDCTokenTypeJWT: + case domain.OIDCTokenTypeJWT: return op.AccessTokenTypeJWT default: return op.AccessTokenTypeBearer } } -func authMethodToOIDC(authType model.OIDCAuthMethodType) oidc.AuthMethod { +func authMethodToOIDC(authType domain.OIDCAuthMethodType) oidc.AuthMethod { switch authType { - case model.OIDCAuthMethodTypeBasic: + case domain.OIDCAuthMethodTypeBasic: return oidc.AuthMethodBasic - case model.OIDCAuthMethodTypePost: + case domain.OIDCAuthMethodTypePost: return oidc.AuthMethodPost - case model.OIDCAuthMethodTypeNone: + case domain.OIDCAuthMethodTypeNone: return oidc.AuthMethodNone - case model.OIDCAuthMethodTypePrivateKeyJWT: + case domain.OIDCAuthMethodTypePrivateKeyJWT: return oidc.AuthMethodPrivateKeyJWT default: return oidc.AuthMethodBasic } } -func responseTypesToOIDC(responseTypes []model.OIDCResponseType) []oidc.ResponseType { +func responseTypesToOIDC(responseTypes []domain.OIDCResponseType) []oidc.ResponseType { oidcTypes := make([]oidc.ResponseType, len(responseTypes)) for i, t := range responseTypes { oidcTypes[i] = responseTypeToOIDC(t) @@ -165,20 +166,20 @@ func responseTypesToOIDC(responseTypes []model.OIDCResponseType) []oidc.Response return oidcTypes } -func responseTypeToOIDC(responseType model.OIDCResponseType) oidc.ResponseType { +func responseTypeToOIDC(responseType domain.OIDCResponseType) oidc.ResponseType { switch responseType { - case model.OIDCResponseTypeCode: + case domain.OIDCResponseTypeCode: return oidc.ResponseTypeCode - case model.OIDCResponseTypeIDTokenToken: + case domain.OIDCResponseTypeIDTokenToken: return oidc.ResponseTypeIDToken - case model.OIDCResponseTypeIDToken: + case domain.OIDCResponseTypeIDToken: return oidc.ResponseTypeIDTokenOnly default: return oidc.ResponseTypeCode } } -func grantTypesToOIDC(grantTypes []model.OIDCGrantType) []oidc.GrantType { +func grantTypesToOIDC(grantTypes []domain.OIDCGrantType) []oidc.GrantType { oidcTypes := make([]oidc.GrantType, len(grantTypes)) for i, t := range grantTypes { oidcTypes[i] = grantTypeToOIDC(t) @@ -186,13 +187,13 @@ func grantTypesToOIDC(grantTypes []model.OIDCGrantType) []oidc.GrantType { return oidcTypes } -func grantTypeToOIDC(grantType model.OIDCGrantType) oidc.GrantType { +func grantTypeToOIDC(grantType domain.OIDCGrantType) oidc.GrantType { switch grantType { - case model.OIDCGrantTypeAuthorizationCode: + case domain.OIDCGrantTypeAuthorizationCode: return oidc.GrantTypeCode - case model.OIDCGrantTypeImplicit: + case domain.OIDCGrantTypeImplicit: return oidc.GrantTypeImplicit - case model.OIDCGrantTypeRefreshToken: + case domain.OIDCGrantTypeRefreshToken: return oidc.GrantTypeRefreshToken default: return oidc.GrantTypeCode diff --git a/internal/auth/repository/application.go b/internal/auth/repository/application.go index 5195f527b..0e9b1076f 100644 --- a/internal/auth/repository/application.go +++ b/internal/auth/repository/application.go @@ -2,11 +2,8 @@ package repository import ( "context" - - "github.com/caos/zitadel/internal/project/model" ) type ApplicationRepository interface { - ApplicationByClientID(ctx context.Context, clientID string) (*model.ApplicationView, error) AuthorizeClientIDSecret(ctx context.Context, clientID, secret string) error } diff --git a/internal/auth/repository/eventsourcing/eventstore/application.go b/internal/auth/repository/eventsourcing/eventstore/application.go index 51a52649b..2a13c31ef 100644 --- a/internal/auth/repository/eventsourcing/eventstore/application.go +++ b/internal/auth/repository/eventsourcing/eventstore/application.go @@ -3,35 +3,25 @@ package eventstore import ( "context" - "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/project/model" - proj_view_model "github.com/caos/zitadel/internal/project/repository/view/model" + "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/telemetry/tracing" ) type ApplicationRepo struct { Commands *command.Commands - View *view.View -} - -func (a *ApplicationRepo) ApplicationByClientID(ctx context.Context, clientID string) (*model.ApplicationView, error) { - app, err := a.View.ApplicationByClientID(ctx, clientID) - if err != nil { - return nil, err - } - return proj_view_model.ApplicationViewToModel(app), nil + Query *query.Queries } func (a *ApplicationRepo) AuthorizeClientIDSecret(ctx context.Context, clientID, secret string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - app, err := a.View.ApplicationByClientID(ctx, clientID) + app, err := a.Query.AppByOIDCClientID(ctx, clientID) if err != nil { return err } - if app.IsOIDC { + if app.OIDCConfig != nil { return a.Commands.VerifyOIDCClientSecret(ctx, app.ProjectID, app.ID, secret) } return a.Commands.VerifyAPIClientSecret(ctx, app.ProjectID, app.ID, secret) diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 6cdb9cfe1..ed9a020f2 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -29,6 +29,7 @@ import ( type AuthRequestRepo struct { Command *command.Commands + Query *query.Queries AuthRequests cache.AuthRequestCache View *view.View Eventstore v1.Eventstore @@ -101,12 +102,12 @@ type orgViewProvider interface { } type userGrantProvider interface { - ApplicationByClientID(context.Context, string) (*project_view_model.ApplicationView, error) + ProjectByOIDCClientID(context.Context, string) (*query.Project, error) UserGrantsByProjectAndUserID(string, string) ([]*grant_view_model.UserGrantView, error) } type projectProvider interface { - ApplicationByClientID(context.Context, string) (*project_view_model.ApplicationView, error) + ProjectByOIDCClientID(context.Context, string) (*query.Project, error) OrgProjectMappingByIDs(orgID, projectID string) (*project_view_model.OrgProjectMapping, error) } @@ -122,18 +123,22 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *dom return nil, err } request.ID = reqID - app, err := repo.View.ApplicationByClientID(ctx, request.ApplicationID) + project, err := repo.ProjectProvider.ProjectByOIDCClientID(ctx, request.ApplicationID) if err != nil { return nil, err } - appIDs, err := repo.View.AppIDsFromProjectID(ctx, app.ProjectID) + projectIDQuery, err := query.NewAppProjectIDSearchQuery(project.ID) + if err != nil { + return nil, err + } + appIDs, err := repo.Query.SearchAppIDs(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) if err != nil { return nil, err } request.Audience = appIDs - request.AppendAudIfNotExisting(app.ProjectID) - request.ApplicationResourceOwner = app.ResourceOwner - request.PrivateLabelingSetting = app.PrivateLabelingSetting + request.AppendAudIfNotExisting(project.ID) + request.ApplicationResourceOwner = project.ResourceOwner + request.PrivateLabelingSetting = project.PrivateLabelingSetting if err := setOrgID(repo.OrgViewProvider, request); err != nil { return nil, err } @@ -1175,20 +1180,20 @@ func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*domain.ExternalUser, } func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *user_model.UserView, userGrantProvider userGrantProvider) (_ bool, err error) { - var app *project_view_model.ApplicationView + var project *query.Project switch request.Request.Type() { case domain.AuthRequestTypeOIDC: - app, err = userGrantProvider.ApplicationByClientID(ctx, request.ApplicationID) + project, err = userGrantProvider.ProjectByOIDCClientID(ctx, request.ApplicationID) if err != nil { return false, err } default: return false, errors.ThrowPreconditionFailed(nil, "EVENT-dfrw2", "Errors.AuthRequest.RequestTypeNotSupported") } - if !app.ProjectRoleCheck { + if !project.ProjectRoleCheck { return false, nil } - grants, err := userGrantProvider.UserGrantsByProjectAndUserID(app.ProjectID, user.ID) + grants, err := userGrantProvider.UserGrantsByProjectAndUserID(project.ID, user.ID) if err != nil { return false, err } @@ -1196,20 +1201,20 @@ func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *u } func projectRequired(ctx context.Context, request *domain.AuthRequest, projectProvider projectProvider) (_ bool, err error) { - var app *project_view_model.ApplicationView + var project *query.Project switch request.Request.Type() { case domain.AuthRequestTypeOIDC: - app, err = projectProvider.ApplicationByClientID(ctx, request.ApplicationID) + project, err = projectProvider.ProjectByOIDCClientID(ctx, request.ApplicationID) if err != nil { return false, err } default: return false, errors.ThrowPreconditionFailed(nil, "EVENT-dfrw2", "Errors.AuthRequest.RequestTypeNotSupported") } - if !app.HasProjectCheck { + if !project.HasProjectCheck { return false, nil } - _, err = projectProvider.OrgProjectMappingByIDs(request.UserOrgID, app.ProjectID) + _, err = projectProvider.OrgProjectMappingByIDs(request.UserOrgID, project.ID) if errors.IsNotFound(err) { return true, nil } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index b7de85759..8bebcb307 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -212,8 +212,8 @@ type mockUserGrants struct { userGrants int } -func (m *mockUserGrants) ApplicationByClientID(ctx context.Context, s string) (*proj_view_model.ApplicationView, error) { - return &proj_view_model.ApplicationView{ProjectRoleCheck: m.roleCheck}, nil +func (m *mockUserGrants) ProjectByOIDCClientID(ctx context.Context, s string) (*query.Project, error) { + return &query.Project{ProjectRoleCheck: m.roleCheck}, nil } func (m *mockUserGrants) UserGrantsByProjectAndUserID(s string, s2 string) ([]*grant_view_model.UserGrantView, error) { @@ -229,8 +229,8 @@ type mockProject struct { projectCheck bool } -func (m *mockProject) ApplicationByClientID(ctx context.Context, s string) (*proj_view_model.ApplicationView, error) { - return &proj_view_model.ApplicationView{HasProjectCheck: m.projectCheck}, nil +func (m *mockProject) ProjectByOIDCClientID(ctx context.Context, s string) (*query.Project, error) { + return &query.Project{HasProjectCheck: m.projectCheck}, nil } func (m *mockProject) OrgProjectMappingByIDs(orgID, projectID string) (*proj_view_model.OrgProjectMapping, error) { diff --git a/internal/auth/repository/eventsourcing/handler/application.go b/internal/auth/repository/eventsourcing/handler/application.go deleted file mode 100644 index 7632180ab..000000000 --- a/internal/auth/repository/eventsourcing/handler/application.go +++ /dev/null @@ -1,168 +0,0 @@ -package handler - -import ( - "context" - "github.com/caos/zitadel/internal/eventstore/v1" - - "github.com/caos/logging" - - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - proj_model "github.com/caos/zitadel/internal/project/model" - es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model" - proj_view "github.com/caos/zitadel/internal/project/repository/view" - view_model "github.com/caos/zitadel/internal/project/repository/view/model" -) - -const ( - applicationTable = "auth.applications" -) - -type Application struct { - handler - subscription *v1.Subscription -} - -func newApplication(handler handler) *Application { - h := &Application{ - handler: handler, - } - - h.subscribe() - - return h -} - -func (a *Application) subscribe() { - a.subscription = a.es.Subscribe(a.AggregateTypes()...) - go func() { - for event := range a.subscription.Events { - query.ReduceEvent(a, event) - } - }() -} - -func (a *Application) ViewModel() string { - return applicationTable -} - -func (a *Application) Subscription() *v1.Subscription { - return a.subscription -} - -func (_ *Application) AggregateTypes() []models.AggregateType { - return []models.AggregateType{es_model.ProjectAggregate} -} - -func (a *Application) CurrentSequence() (uint64, error) { - sequence, err := a.view.GetLatestApplicationSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (a *Application) EventQuery() (*models.SearchQuery, error) { - sequence, err := a.view.GetLatestApplicationSequence() - if err != nil { - return nil, err - } - return proj_view.ProjectQuery(sequence.CurrentSequence), nil -} - -func (a *Application) Reduce(event *models.Event) (err error) { - app := new(view_model.ApplicationView) - switch event.Type { - case es_model.ApplicationAdded: - project, err := a.getProjectByID(context.Background(), event.AggregateID) - if err != nil { - return err - } - app.ProjectRoleCheck = project.ProjectRoleCheck - app.HasProjectCheck = project.HasProjectCheck - app.ProjectRoleAssertion = project.ProjectRoleAssertion - app.PrivateLabelingSetting = project.PrivateLabelingSetting - - err = app.AppendEvent(event) - case es_model.ApplicationChanged, - es_model.OIDCConfigAdded, - es_model.OIDCConfigChanged, - es_model.APIConfigAdded, - es_model.APIConfigChanged, - es_model.ApplicationDeactivated, - es_model.ApplicationReactivated: - err = app.SetData(event) - if err != nil { - return err - } - app, err = a.view.ApplicationByID(event.AggregateID, app.ID) - if err != nil { - return err - } - err = app.AppendEvent(event) - case es_model.ApplicationRemoved: - err = app.SetData(event) - if err != nil { - return err - } - return a.view.DeleteApplication(app.ID, event) - case es_model.ProjectChanged: - apps, err := a.view.ApplicationsByProjectID(event.AggregateID) - if err != nil { - return err - } - if len(apps) == 0 { - return a.view.ProcessedApplicationSequence(event) - } - for _, app := range apps { - if err := app.AppendEvent(event); err != nil { - return err - } - } - return a.view.PutApplications(apps, event) - case es_model.ProjectRemoved: - err = a.view.DeleteApplicationsByProjectID(event.AggregateID) - if err == nil { - return a.view.ProcessedApplicationSequence(event) - } - default: - return a.view.ProcessedApplicationSequence(event) - } - if err != nil { - return err - } - return a.view.PutApplication(app, event) -} - -func (a *Application) OnError(event *models.Event, spoolerError error) error { - logging.LogWithFields("SPOOL-ls9ew", "id", event.AggregateID).WithError(spoolerError).Warn("something went wrong in project app handler") - return spooler.HandleError(event, spoolerError, a.view.GetLatestApplicationFailedEvent, a.view.ProcessedApplicationFailedEvent, a.view.ProcessedApplicationSequence, a.errorCountUntilSkip) -} - -func (a *Application) OnSuccess() error { - return spooler.HandleSuccess(a.view.UpdateApplicationSpoolerRunTimestamp) -} - -func (a *Application) getProjectByID(ctx context.Context, projID string) (*proj_model.Project, error) { - query, err := proj_view.ProjectByIDQuery(projID, 0) - if err != nil { - return nil, err - } - esProject := &es_model.Project{ - ObjectRoot: models.ObjectRoot{ - AggregateID: projID, - }, - } - err = es_sdk.Filter(ctx, a.Eventstore().FilterEvents, esProject.AppendEvents, query) - if err != nil && !errors.IsNotFound(err) { - return nil, err - } - if esProject.Sequence == 0 { - return nil, errors.ThrowNotFound(nil, "EVENT-DBf32", "Errors.Project.NotFound") - } - - return es_model.ProjectToModel(esProject), nil -} diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 409910aaa..e1196acb1 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -44,7 +44,6 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es newKey( handler{view, bulkLimit, configs.cycleDuration("Key"), errorCount, es}, keyChan), - newApplication(handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount, es}), newUserGrant( handler{view, bulkLimit, configs.cycleDuration("UserGrant"), errorCount, es}, systemDefaults.IamID), diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index acd368b30..0c3427648 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -68,7 +68,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co assetsAPI := conf.APIDomain + "/assets/v1/" - view, err := auth_view.StartView(sqlClient, keyAlgorithm, idGenerator, assetsAPI) + view, err := auth_view.StartView(sqlClient, keyAlgorithm, queries, idGenerator, assetsAPI) if err != nil { return nil, err } @@ -92,6 +92,14 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co SystemDefaults: systemDefaults, PrefixAvatarURL: assetsAPI, } + //TODO: remove as soon as possible + queryView := struct { + *query.Queries + *auth_view.View + }{ + queries, + view, + } return &EsRepository{ spool, es, @@ -111,8 +119,9 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co IDPProviderViewProvider: view, LockoutPolicyViewProvider: queries, LoginPolicyViewProvider: queries, - UserGrantProvider: view, - ProjectProvider: view, + Query: queries, + UserGrantProvider: queryView, + ProjectProvider: queryView, IdGenerator: idGenerator, PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, ExternalLoginCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, @@ -143,7 +152,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co }, eventstore.ApplicationRepo{ Commands: command, - View: view, + Query: queries, }, eventstore.UserSessionRepo{ diff --git a/internal/auth/repository/eventsourcing/view/application.go b/internal/auth/repository/eventsourcing/view/application.go deleted file mode 100644 index bbfb6c094..000000000 --- a/internal/auth/repository/eventsourcing/view/application.go +++ /dev/null @@ -1,133 +0,0 @@ -package view - -import ( - "context" - "github.com/caos/zitadel/internal/domain" - - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - proj_model "github.com/caos/zitadel/internal/project/model" - "github.com/caos/zitadel/internal/project/repository/view" - "github.com/caos/zitadel/internal/project/repository/view/model" - "github.com/caos/zitadel/internal/view/repository" -) - -const ( - applicationTable = "auth.applications" -) - -func (v *View) ApplicationByID(projectID, appID string) (*model.ApplicationView, error) { - return view.ApplicationByID(v.Db, applicationTable, projectID, appID) -} - -func (v *View) ApplicationsByProjectID(projectID string) ([]*model.ApplicationView, error) { - return view.ApplicationsByProjectID(v.Db, applicationTable, projectID) -} - -func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, uint64, error) { - return view.SearchApplications(v.Db, applicationTable, request) -} - -func (v *View) PutApplication(app *model.ApplicationView, event *models.Event) error { - err := view.PutApplication(v.Db, applicationTable, app) - if err != nil { - return err - } - return v.ProcessedApplicationSequence(event) -} - -func (v *View) PutApplications(apps []*model.ApplicationView, event *models.Event) error { - err := view.PutApplications(v.Db, applicationTable, apps...) - if err != nil { - return err - } - return v.ProcessedApplicationSequence(event) -} - -func (v *View) DeleteApplication(appID string, event *models.Event) error { - err := view.DeleteApplication(v.Db, applicationTable, appID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedApplicationSequence(event) -} - -func (v *View) DeleteApplicationsByProjectID(projectID string) error { - return view.DeleteApplicationsByProjectID(v.Db, applicationTable, projectID) -} - -func (v *View) GetLatestApplicationSequence() (*repository.CurrentSequence, error) { - return v.latestSequence(applicationTable) -} - -func (v *View) ProcessedApplicationSequence(event *models.Event) error { - return v.saveCurrentSequence(applicationTable, event) -} - -func (v *View) UpdateApplicationSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(applicationTable) -} - -func (v *View) GetLatestApplicationFailedEvent(sequence uint64) (*repository.FailedEvent, error) { - return v.latestFailedEvent(applicationTable, sequence) -} - -func (v *View) ProcessedApplicationFailedEvent(failedEvent *repository.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} - -func (v *View) ApplicationByClientID(_ context.Context, clientID string) (*model.ApplicationView, error) { - return view.ApplicationByOIDCClientID(v.Db, applicationTable, clientID) -} - -func (v *View) AppIDsFromProjectByClientID(ctx context.Context, clientID string) ([]string, error) { - app, err := v.ApplicationByClientID(ctx, clientID) - if err != nil { - return nil, err - } - req := &proj_model.ApplicationSearchRequest{ - Queries: []*proj_model.ApplicationSearchQuery{ - { - Key: proj_model.AppSearchKeyProjectID, - Method: domain.SearchMethodEquals, - Value: app.ProjectID, - }, - }, - } - apps, _, err := view.SearchApplications(v.Db, applicationTable, req) - if err != nil { - return nil, errors.ThrowPreconditionFailed(err, "VIEW-Gd24q", "cannot find applications") - } - ids := make([]string, 0, len(apps)) - for _, app := range apps { - if !app.IsOIDC { - continue - } - ids = append(ids, app.OIDCClientID) - } - return ids, nil -} - -func (v *View) AppIDsFromProjectID(ctx context.Context, projectID string) ([]string, error) { - req := &proj_model.ApplicationSearchRequest{ - Queries: []*proj_model.ApplicationSearchQuery{ - { - Key: proj_model.AppSearchKeyProjectID, - Method: domain.SearchMethodEquals, - Value: projectID, - }, - }, - } - apps, _, err := view.SearchApplications(v.Db, applicationTable, req) - if err != nil { - return nil, errors.ThrowPreconditionFailed(err, "VIEW-Gd24q", "cannot find applications") - } - ids := make([]string, 0, len(apps)) - for _, app := range apps { - if !app.IsOIDC { - continue - } - ids = append(ids, app.OIDCClientID) - } - return ids, nil -} diff --git a/internal/auth/repository/eventsourcing/view/view.go b/internal/auth/repository/eventsourcing/view/view.go index 7016ec9b9..d02019f68 100644 --- a/internal/auth/repository/eventsourcing/view/view.go +++ b/internal/auth/repository/eventsourcing/view/view.go @@ -7,6 +7,7 @@ import ( "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/id" + "github.com/caos/zitadel/internal/query" ) type View struct { @@ -14,9 +15,10 @@ type View struct { keyAlgorithm crypto.EncryptionAlgorithm idGenerator id.Generator prefixAvatarURL string + query *query.Queries } -func StartView(sqlClient *sql.DB, keyAlgorithm crypto.EncryptionAlgorithm, idGenerator id.Generator, prefixAvatarURL string) (*View, error) { +func StartView(sqlClient *sql.DB, keyAlgorithm crypto.EncryptionAlgorithm, queries *query.Queries, idGenerator id.Generator, prefixAvatarURL string) (*View, error) { gorm, err := gorm.Open("postgres", sqlClient) if err != nil { return nil, err @@ -26,6 +28,7 @@ func StartView(sqlClient *sql.DB, keyAlgorithm crypto.EncryptionAlgorithm, idGen keyAlgorithm: keyAlgorithm, idGenerator: idGenerator, prefixAvatarURL: prefixAvatarURL, + query: queries, }, nil } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index 1d1d59780..2d50dd026 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -66,7 +66,7 @@ func (repo *TokenVerifierRepo) TokenByID(ctx context.Context, tokenID, userID st return model.TokenViewToModel(token), nil } -func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenString, verifierClientID string) (userID string, agentID string, clientID, prefLang, resourceOwner string, err error) { +func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenString, verifierClientID, projectID string) (userID string, agentID string, clientID, prefLang, resourceOwner string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() tokenData, err := base64.RawURLEncoding.DecodeString(tokenString) @@ -90,10 +90,6 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin return "", "", "", "", "", caos_errs.ThrowUnauthenticated(err, "APP-k9KS0", "invalid token") } - projectID, _, err := repo.ProjectIDAndOriginsByClientID(ctx, verifierClientID) - if err != nil { - return "", "", "", "", "", caos_errs.ThrowUnauthenticated(err, "APP-5M9so", "invalid token") - } for _, aud := range token.Audience { if verifierClientID == aud || projectID == aud { return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil @@ -107,7 +103,7 @@ func (repo *TokenVerifierRepo) ProjectIDAndOriginsByClientID(ctx context.Context if err != nil { return "", nil, err } - return app.ProjectID, app.OriginAllowList, nil + return app.ProjectID, app.OIDCConfig.AllowedOrigins, nil } func (repo *TokenVerifierRepo) CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error { @@ -237,19 +233,24 @@ func MissingFeatureErr(feature string) error { return caos_errs.ThrowPermissionDeniedf(nil, "AUTH-Dvgsf", "missing feature %v", feature) } -func (repo *TokenVerifierRepo) VerifierClientID(ctx context.Context, appName string) (_ string, err error) { +func (repo *TokenVerifierRepo) VerifierClientID(ctx context.Context, appName string) (clientID, projectID string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() iam, err := repo.getIAMByID(ctx) if err != nil { - return "", err + return "", "", err } app, err := repo.View.ApplicationByProjecIDAndAppName(ctx, iam.IAMProjectID, appName) if err != nil { - return "", err + return "", "", err } - return app.OIDCClientID, nil + if app.OIDCConfig != nil { + clientID = app.OIDCConfig.ClientID + } else if app.APIConfig != nil { + clientID = app.APIConfig.ClientID + } + return clientID, app.ProjectID, nil } func (r *TokenVerifierRepo) getUserEvents(ctx context.Context, userID string, sequence uint64) ([]*models.Event, error) { diff --git a/internal/authz/repository/eventsourcing/handler/application.go b/internal/authz/repository/eventsourcing/handler/application.go deleted file mode 100644 index c70798506..000000000 --- a/internal/authz/repository/eventsourcing/handler/application.go +++ /dev/null @@ -1,114 +0,0 @@ -package handler - -import ( - "github.com/caos/logging" - "github.com/caos/zitadel/internal/eventstore/v1" - - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model" - "github.com/caos/zitadel/internal/project/repository/view" - view_model "github.com/caos/zitadel/internal/project/repository/view/model" -) - -const ( - applicationTable = "authz.applications" -) - -type Application struct { - handler - subscription *v1.Subscription -} - -func newApplication(handler handler) *Application { - h := &Application{ - handler: handler, - } - - h.subscribe() - - return h -} - -func (k *Application) subscribe() { - k.subscription = k.es.Subscribe(k.AggregateTypes()...) - go func() { - for event := range k.subscription.Events { - query.ReduceEvent(k, event) - } - }() -} - -func (a *Application) ViewModel() string { - return applicationTable -} - -func (p *Application) Subscription() *v1.Subscription { - return p.subscription -} - -func (a *Application) AggregateTypes() []models.AggregateType { - return []models.AggregateType{es_model.ProjectAggregate} -} - -func (a *Application) CurrentSequence() (uint64, error) { - sequence, err := a.view.GetLatestApplicationSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (a *Application) EventQuery() (*models.SearchQuery, error) { - sequence, err := a.view.GetLatestApplicationSequence() - if err != nil { - return nil, err - } - return view.ProjectQuery(sequence.CurrentSequence), nil -} - -func (a *Application) Reduce(event *models.Event) (err error) { - app := new(view_model.ApplicationView) - switch event.Type { - case es_model.ApplicationAdded: - app.AppendEvent(event) - case es_model.ApplicationChanged, - es_model.OIDCConfigAdded, - es_model.OIDCConfigChanged, - es_model.APIConfigAdded, - es_model.APIConfigChanged, - es_model.ApplicationDeactivated, - es_model.ApplicationReactivated: - err := app.SetData(event) - if err != nil { - return err - } - app, err = a.view.ApplicationByID(event.AggregateID, app.ID) - if err != nil { - return err - } - app.AppendEvent(event) - case es_model.ApplicationRemoved: - err := app.SetData(event) - if err != nil { - return err - } - return a.view.DeleteApplication(app.ID, event) - default: - return a.view.ProcessedApplicationSequence(event) - } - if err != nil { - return err - } - return a.view.PutApplication(app, event) -} - -func (a *Application) OnError(event *models.Event, spoolerError error) error { - logging.LogWithFields("SPOOL-sjZw", "id", event.AggregateID).WithError(spoolerError).Warn("something went wrong in project app handler") - return spooler.HandleError(event, spoolerError, a.view.GetLatestApplicationFailedEvent, a.view.ProcessedApplicationFailedEvent, a.view.ProcessedApplicationSequence, a.errorCountUntilSkip) -} - -func (a *Application) OnSuccess() error { - return spooler.HandleSuccess(a.view.UpdateApplicationSpoolerRunTimestamp) -} diff --git a/internal/authz/repository/eventsourcing/handler/handler.go b/internal/authz/repository/eventsourcing/handler/handler.go index f8fb96153..47ab4c66a 100644 --- a/internal/authz/repository/eventsourcing/handler/handler.go +++ b/internal/authz/repository/eventsourcing/handler/handler.go @@ -36,8 +36,6 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es systemDefaults.IamID), newUserMembership( handler{view, bulkLimit, configs.cycleDuration("UserMemberships"), errorCount, es}), - newApplication( - handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount, es}), } } diff --git a/internal/authz/repository/eventsourcing/repository.go b/internal/authz/repository/eventsourcing/repository.go index 79c7b07a0..24bfc837c 100644 --- a/internal/authz/repository/eventsourcing/repository.go +++ b/internal/authz/repository/eventsourcing/repository.go @@ -4,7 +4,7 @@ import ( "context" "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/eventstore/v1" + v1 "github.com/caos/zitadel/internal/eventstore/v1" "github.com/caos/zitadel/internal/query" @@ -43,7 +43,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, qu } idGenerator := id.SonyFlakeGenerator - view, err := authz_view.StartView(sqlClient, idGenerator) + view, err := authz_view.StartView(sqlClient, idGenerator, queries) if err != nil { return nil, err } diff --git a/internal/authz/repository/eventsourcing/view/application.go b/internal/authz/repository/eventsourcing/view/application.go index 1b1b66c69..1281fe2d4 100644 --- a/internal/authz/repository/eventsourcing/view/application.go +++ b/internal/authz/repository/eventsourcing/view/application.go @@ -4,69 +4,45 @@ import ( "context" "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - proj_model "github.com/caos/zitadel/internal/project/model" - "github.com/caos/zitadel/internal/project/repository/view" - "github.com/caos/zitadel/internal/project/repository/view/model" + "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/telemetry/tracing" - "github.com/caos/zitadel/internal/view/repository" ) -const ( - applicationTable = "authz.applications" -) - -func (v *View) ApplicationByID(projectID, appID string) (*model.ApplicationView, error) { - return view.ApplicationByID(v.Db, applicationTable, projectID, appID) +func (v *View) ApplicationByOIDCClientID(clientID string) (*query.App, error) { + return v.Query.AppByOIDCClientID(context.TODO(), clientID) } -func (v *View) ApplicationByOIDCClientID(clientID string) (*model.ApplicationView, error) { - return view.ApplicationByOIDCClientID(v.Db, applicationTable, clientID) -} - -func (v *View) ApplicationByProjecIDAndAppName(ctx context.Context, projectID, appName string) (_ *model.ApplicationView, err error) { +func (v *View) ApplicationByProjecIDAndAppName(ctx context.Context, projectID, appName string) (_ *query.App, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - return view.ApplicationByProjectIDAndAppName(v.Db, applicationTable, projectID, appName) -} - -func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, uint64, error) { - return view.SearchApplications(v.Db, applicationTable, request) -} - -func (v *View) PutApplication(project *model.ApplicationView, event *models.Event) error { - err := view.PutApplication(v.Db, applicationTable, project) + nameQuery, err := query.NewAppNameSearchQuery(query.TextEquals, appName) if err != nil { - return err + return nil, err } - return v.ProcessedApplicationSequence(event) -} - -func (v *View) DeleteApplication(appID string, event *models.Event) error { - err := view.DeleteApplication(v.Db, applicationTable, appID) - if err != nil && !errors.IsNotFound(err) { - return err + projectQuery, err := query.NewAppProjectIDSearchQuery(projectID) + if err != nil { + return nil, err } - return v.ProcessedApplicationSequence(event) + + queries := &query.AppSearchQueries{ + Queries: []query.SearchQuery{ + nameQuery, + projectQuery, + }, + } + + apps, err := v.Query.SearchApps(ctx, queries) + if err != nil { + return nil, err + } + if len(apps.Apps) != 1 { + return nil, errors.ThrowNotFound(nil, "VIEW-svLQq", "app not found") + } + + return apps.Apps[0], nil } -func (v *View) GetLatestApplicationSequence() (*repository.CurrentSequence, error) { - return v.latestSequence(applicationTable) -} - -func (v *View) ProcessedApplicationSequence(event *models.Event) error { - return v.saveCurrentSequence(applicationTable, event) -} - -func (v *View) UpdateApplicationSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(applicationTable) -} - -func (v *View) GetLatestApplicationFailedEvent(sequence uint64) (*repository.FailedEvent, error) { - return v.latestFailedEvent(applicationTable, sequence) -} - -func (v *View) ProcessedApplicationFailedEvent(failedEvent *repository.FailedEvent) error { - return v.saveFailedEvent(failedEvent) +func (v *View) SearchApplications(request *query.AppSearchQueries) (*query.Apps, error) { + return v.Query.SearchApps(context.TODO(), request) } diff --git a/internal/authz/repository/eventsourcing/view/view.go b/internal/authz/repository/eventsourcing/view/view.go index 61799ad36..32f5c2afb 100644 --- a/internal/authz/repository/eventsourcing/view/view.go +++ b/internal/authz/repository/eventsourcing/view/view.go @@ -2,17 +2,20 @@ package view import ( "database/sql" + "github.com/caos/zitadel/internal/id" + "github.com/caos/zitadel/internal/query" "github.com/jinzhu/gorm" ) type View struct { Db *gorm.DB + Query *query.Queries idGenerator id.Generator } -func StartView(sqlClient *sql.DB, idGenerator id.Generator) (*View, error) { +func StartView(sqlClient *sql.DB, idGenerator id.Generator, queries *query.Queries) (*View, error) { gorm, err := gorm.Open("postgres", sqlClient) if err != nil { return nil, err @@ -20,6 +23,7 @@ func StartView(sqlClient *sql.DB, idGenerator id.Generator) (*View, error) { return &View{ Db: gorm, idGenerator: idGenerator, + Query: queries, }, nil } diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go index 40b12ff5b..41cee2392 100644 --- a/internal/domain/application_oidc.go +++ b/internal/domain/application_oidc.go @@ -181,16 +181,38 @@ func GetOIDCCompliance(version OIDCVersion, appType OIDCApplicationType, grantTy case OIDCVersionV1: return GetOIDCV1Compliance(appType, grantTypes, authMethod, redirectUris) } - return nil + return &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.UnsupportedVersion"}, + } } func GetOIDCV1Compliance(appType OIDCApplicationType, grantTypes []OIDCGrantType, authMethod OIDCAuthMethodType, redirectUris []string) *Compliance { compliance := &Compliance{NoneCompliant: false} - if redirectUris == nil || len(redirectUris) == 0 { + + checkGrantTypesCombination(compliance, grantTypes) + checkRedirectURIs(compliance, grantTypes, appType, redirectUris) + checkApplicaitonType(compliance, appType, authMethod) + + if compliance.NoneCompliant { + compliance.Problems = append([]string{"Application.OIDC.V1.NotCompliant"}, compliance.Problems...) + } + return compliance +} + +func checkGrantTypesCombination(compliance *Compliance, grantTypes []OIDCGrantType) { + if containsOIDCGrantType(grantTypes, OIDCGrantTypeRefreshToken) && !containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode) { + compliance.NoneCompliant = true + compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.GrantType.Refresh.NoAuthCode") + } +} + +func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appType OIDCApplicationType, redirectUris []string) { + if len(redirectUris) == 0 { compliance.NoneCompliant = true compliance.Problems = append([]string{"Application.OIDC.V1.NoRedirectUris"}, compliance.Problems...) } - CheckGrantTypes(compliance, grantTypes) + if containsOIDCGrantType(grantTypes, OIDCGrantTypeImplicit) && containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode) { CheckRedirectUrisImplicitAndCode(compliance, appType, redirectUris) } else { @@ -201,7 +223,9 @@ func GetOIDCV1Compliance(appType OIDCApplicationType, grantTypes []OIDCGrantType CheckRedirectUrisCode(compliance, appType, redirectUris) } } +} +func checkApplicaitonType(compliance *Compliance, appType OIDCApplicationType, authMethod OIDCAuthMethodType) { switch appType { case OIDCApplicationTypeNative: GetOIDCV1NativeApplicationCompliance(compliance, authMethod) @@ -211,14 +235,6 @@ func GetOIDCV1Compliance(appType OIDCApplicationType, grantTypes []OIDCGrantType if compliance.NoneCompliant { compliance.Problems = append([]string{"Application.OIDC.V1.NotCompliant"}, compliance.Problems...) } - return compliance -} - -func CheckGrantTypes(compliance *Compliance, grantTypes []OIDCGrantType) { - if containsOIDCGrantType(grantTypes, OIDCGrantTypeRefreshToken) && !containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode) { - compliance.NoneCompliant = true - compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.GrantType.Refresh.NoAuthCode") - } } func GetOIDCV1NativeApplicationCompliance(compliance *Compliance, authMethod OIDCAuthMethodType) { @@ -345,3 +361,22 @@ func isHTTPLoopbackLocalhost(uri string) bool { strings.HasPrefix(uri, httpLoopbackV6LongWithoutPort) || strings.HasPrefix(uri, httpLoopbackV6LongWithPort) } + +func OIDCOriginAllowList(redirectURIs, additionalOrigins []string) ([]string, error) { + allowList := make([]string, 0) + for _, redirect := range redirectURIs { + origin, err := http_util.GetOriginFromURLString(redirect) + if err != nil { + return nil, err + } + if !http_util.IsOriginAllowed(allowList, origin) { + allowList = append(allowList, origin) + } + } + for _, origin := range additionalOrigins { + if !http_util.IsOriginAllowed(allowList, origin) { + allowList = append(allowList, origin) + } + } + return allowList, nil +} diff --git a/internal/domain/application_oidc_test.go b/internal/domain/application_oidc_test.go index 63aee9159..fd2b5c117 100644 --- a/internal/domain/application_oidc_test.go +++ b/internal/domain/application_oidc_test.go @@ -1,6 +1,8 @@ package domain import ( + "reflect" + "strings" "testing" "time" @@ -185,3 +187,489 @@ func TestApplicationValid(t *testing.T) { }) } } + +func TestGetOIDCV1Compliance(t *testing.T) { + type args struct { + appType OIDCApplicationType + grantTypes []OIDCGrantType + authMethod OIDCAuthMethodType + redirectUris []string + } + tests := []struct { + name string + args args + }{ + { + name: "none compliant", + args: args{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetOIDCV1Compliance(tt.args.appType, tt.args.grantTypes, tt.args.authMethod, tt.args.redirectUris) + if !got.NoneCompliant { + t.Error("compliance should be none compliant") + } + if len(got.Problems) == 0 || got.Problems[0] != "Application.OIDC.V1.NotCompliant" { + t.Errorf("first entry of problems should be \"Application.OIDC.V1.NotCompliant\" but got %v", got.Problems) + } + }) + } +} + +func Test_checkGrantTypesCombination(t *testing.T) { + tests := []struct { + name string + want *Compliance + grantTypes []OIDCGrantType + }{ + { + name: "implicit", + want: new(Compliance), + grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit}, + }, + { + name: "refresh token and implicit", + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.GrantType.Refresh.NoAuthCode"}, + }, + grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit, OIDCGrantTypeRefreshToken}, + }, + { + name: "refresh token and authorization code", + want: &Compliance{}, + grantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeRefreshToken}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compliance := new(Compliance) + + checkGrantTypesCombination(compliance, tt.grantTypes) + + if tt.want.NoneCompliant != compliance.NoneCompliant { + t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, compliance.NoneCompliant) + } + if !reflect.DeepEqual(tt.want.Problems, compliance.Problems) { + t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, compliance.Problems) + } + }) + } +} + +func Test_checkRedirectURIs(t *testing.T) { + type args struct { + grantTypes []OIDCGrantType + appType OIDCApplicationType + redirectUris []string + } + tests := []struct { + name string + want *Compliance + args args + }{ + { + name: "no redirect uris", + want: &Compliance{ + NoneCompliant: true, + Problems: []string{ + "Application.OIDC.V1.NoRedirectUris", + }, + }, + args: args{}, + }, + { + name: "implicit and authorization code", + want: &Compliance{ + NoneCompliant: false, + Problems: []string{"Application.OIDC.V1.NotAllCombinationsAreAllowed"}, + }, + args: args{ + redirectUris: []string{"http://redirect.to/me"}, + grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit, OIDCGrantTypeAuthorizationCode}, + }, + }, + { + name: "only implicit", + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.HttpNotAllowed"}, + }, + args: args{ + redirectUris: []string{"http://redirect.to/me"}, + grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit}, + appType: OIDCApplicationTypeUserAgent, + }, + }, + { + name: "only authorization code", + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb"}, + }, + args: args{ + redirectUris: []string{"http://redirect.to/me"}, + grantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, + appType: OIDCApplicationTypeUserAgent, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compliance := new(Compliance) + + checkRedirectURIs(compliance, tt.args.grantTypes, tt.args.appType, tt.args.redirectUris) + + if tt.want.NoneCompliant != compliance.NoneCompliant { + t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, compliance.NoneCompliant) + } + if !reflect.DeepEqual(tt.want.Problems, compliance.Problems) { + t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, compliance.Problems) + } + }) + } +} + +func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) { + type args struct { + appType OIDCApplicationType + redirectUris []string + } + tests := []struct { + name string + want *Compliance + args args + }{ + { + name: "implicit and code https", + want: &Compliance{ + NoneCompliant: false, + Problems: nil, + }, + args: args{ + redirectUris: []string{"https://redirect.to/me"}, + }, + }, + // { + // name: "custom protocol, not native", + // want: &Compliance{ + // NoneCompliant: true, + // Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed"}, + // }, + // args: args{ + // redirectUris: []string{"protocol://redirect.to/me"}, + // appType: OIDCApplicationTypeWeb, + // }, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compliance := new(Compliance) + + CheckRedirectUrisImplicitAndCode(compliance, tt.args.appType, tt.args.redirectUris) + + if tt.want.NoneCompliant != compliance.NoneCompliant { + t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, compliance.NoneCompliant) + } + if !reflect.DeepEqual(tt.want.Problems, compliance.Problems) { + t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, compliance.Problems) + } + }) + } +} + +func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { + type args struct { + appType OIDCApplicationType + redirectUris []string + } + tests := []struct { + name string + args args + want *Compliance + }{ + { + name: "only https", + args: args{}, + want: &Compliance{}, + }, + { + name: "custom protocol not native app", + args: args{ + appType: OIDCApplicationTypeWeb, + redirectUris: []string{"custom://nirvana.com"}, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed"}, + }, + }, + { + name: "http localhost user agent app", + args: args{ + appType: OIDCApplicationTypeUserAgent, + redirectUris: []string{"http://localhost:9009"}, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb"}, + }, + }, + { + name: "http, not only localhost native app", + args: args{ + appType: OIDCApplicationTypeNative, + redirectUris: []string{"http://nirvana.com", "http://localhost:9009"}, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost"}, + }, + }, + { + name: "not allowed combination", + args: args{ + appType: OIDCApplicationTypeNative, + redirectUris: []string{"https://nirvana.com", "cutom://nirvana.com"}, + }, + want: &Compliance{ + NoneCompliant: false, + Problems: []string{"Application.OIDC.V1.NotAllCombinationsAreAllowed"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := new(Compliance) + CheckRedirectUrisImplicitAndCode(got, tt.args.appType, tt.args.redirectUris) + + if tt.want.NoneCompliant != got.NoneCompliant { + t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, got.NoneCompliant) + } + if !reflect.DeepEqual(tt.want.Problems, got.Problems) { + t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, got.Problems) + } + }) + } +} + +func TestCheckRedirectUrisImplicit(t *testing.T) { + type args struct { + appType OIDCApplicationType + redirectUris []string + } + tests := []struct { + name string + args args + want *Compliance + }{ + { + name: "only https", + args: args{}, + want: &Compliance{}, + }, + { + name: "custom protocol", + args: args{ + redirectUris: []string{"custom://nirvana.com"}, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed"}, + }, + }, + { + name: "only http protocol, app type native, not only localhost", + args: args{ + redirectUris: []string{"http://nirvana.com"}, + appType: OIDCApplicationTypeNative, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost"}, + }, + }, + { + name: "only http protocol, app type native, only localhost", + args: args{ + redirectUris: []string{"http://localhost:8080"}, + appType: OIDCApplicationTypeNative, + }, + want: &Compliance{ + NoneCompliant: false, + Problems: nil, + }, + }, + { + name: "only http protocol, app type web", + args: args{ + redirectUris: []string{"http://nirvana.com"}, + appType: OIDCApplicationTypeWeb, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.HttpNotAllowed"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := new(Compliance) + CheckRedirectUrisImplicit(got, tt.args.appType, tt.args.redirectUris) + + if tt.want.NoneCompliant != got.NoneCompliant { + t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, got.NoneCompliant) + } + if !reflect.DeepEqual(tt.want.Problems, got.Problems) { + t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, got.Problems) + } + }) + } +} + +func TestCheckRedirectUrisCode(t *testing.T) { + type args struct { + appType OIDCApplicationType + redirectUris []string + } + tests := []struct { + name string + args args + want *Compliance + }{ + { + name: "only https", + args: args{}, + want: &Compliance{}, + }, + { + name: "custom prefix, app type web", + args: args{ + redirectUris: []string{"custom://nirvana.com"}, + appType: OIDCApplicationTypeWeb, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Code.RedirectUris.CustomOnlyForNative"}, + }, + }, + { + name: "only http protocol, app type user agent", + args: args{ + redirectUris: []string{"http://nirvana.com"}, + appType: OIDCApplicationTypeUserAgent, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb"}, + }, + }, + { + name: "only http protocol, app type native, only localhost", + args: args{ + redirectUris: []string{"http://localhost:8080", "http://nirvana.com:8080"}, + appType: OIDCApplicationTypeNative, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost"}, + }, + }, + { + name: "custom protocol, not native", + args: args{ + redirectUris: []string{"custom://nirvana.com"}, + appType: OIDCApplicationTypeWeb, + }, + want: &Compliance{ + NoneCompliant: true, + Problems: []string{"Application.OIDC.V1.Code.RedirectUris.CustomOnlyForNative"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := new(Compliance) + CheckRedirectUrisCode(got, tt.args.appType, tt.args.redirectUris) + + if tt.want.NoneCompliant != got.NoneCompliant { + t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, got.NoneCompliant) + } + if !reflect.DeepEqual(tt.want.Problems, got.Problems) { + t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, got.Problems) + } + }) + } +} + +func TestOIDCOriginAllowList(t *testing.T) { + type args struct { + redirectUris []string + additionalOrigins []string + } + type want struct { + allowed []string + err func(error) bool + } + tests := []struct { + name string + args args + want want + }{ + { + name: "no uris, no origins", + args: args{}, + want: want{ + allowed: []string{}, + }, + }, + { + name: "redirects invalid schema", + args: args{ + redirectUris: []string{"https:// localhost:8080"}, + }, + want: want{ + allowed: nil, + err: func(e error) bool { + return strings.HasPrefix(e.Error(), "invalid chavalid character") + }, + }, + }, + { + name: "redirects additional", + args: args{ + redirectUris: []string{"https://localhost:8080"}, + }, + want: want{ + allowed: []string{"https://localhost:8080"}, + }, + }, + { + name: "additional origin", + args: args{ + additionalOrigins: []string{"https://localhost:8080"}, + }, + want: want{ + allowed: []string{"https://localhost:8080"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + allowed, err := OIDCOriginAllowList(tt.args.redirectUris, tt.args.additionalOrigins) + + if tt.want.err == nil && err != nil { + t.Errorf("unexpected error: %v", err) + } else if tt.want.err == nil && err == nil { + //ok + } else if tt.want.err(err) { + t.Errorf("unexpected err got %v", err) + } + + if !reflect.DeepEqual(allowed, tt.want.allowed) { + t.Errorf("expected list: %v, got: %v", tt.want.allowed, allowed) + } + }) + } +} diff --git a/internal/management/repository/eventsourcing/eventstore/project.go b/internal/management/repository/eventsourcing/eventstore/project.go index 762d0a9ff..7d4c1db12 100644 --- a/internal/management/repository/eventsourcing/eventstore/project.go +++ b/internal/management/repository/eventsourcing/eventstore/project.go @@ -93,63 +93,6 @@ func (repo *ProjectRepo) ProjectChanges(ctx context.Context, id string, lastSequ return changes, nil } -func (repo *ProjectRepo) ApplicationByID(ctx context.Context, projectID, appID string) (*proj_model.ApplicationView, error) { - app, viewErr := repo.View.ApplicationByID(projectID, appID) - if viewErr != nil && !caos_errs.IsNotFound(viewErr) { - return nil, viewErr - } - if caos_errs.IsNotFound(viewErr) { - app = new(model.ApplicationView) - app.ID = appID - } - - events, esErr := repo.getProjectEvents(ctx, projectID, app.Sequence) - if caos_errs.IsNotFound(viewErr) && len(events) == 0 { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-Fshu8", "Errors.Application.NotFound") - } - - if esErr != nil { - logging.Log("EVENT-SLCo9").WithError(viewErr).Debug("error retrieving new events") - return model.ApplicationViewToModel(app), nil - } - - viewApp := *app - for _, event := range events { - err := app.AppendEventIfMyApp(event) - if err != nil { - return model.ApplicationViewToModel(&viewApp), nil - } - if app.State == int32(proj_model.AppStateRemoved) { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-Msl96", "Errors.Application.NotFound") - } - } - return model.ApplicationViewToModel(app), nil -} - -func (repo *ProjectRepo) SearchApplications(ctx context.Context, request *proj_model.ApplicationSearchRequest) (*proj_model.ApplicationSearchResponse, error) { - err := request.EnsureLimit(repo.SearchLimit) - if err != nil { - return nil, err - } - sequence, sequenceErr := repo.View.GetLatestApplicationSequence() - logging.Log("EVENT-SKe8s").OnError(sequenceErr).Warn("could not read latest application sequence") - apps, count, err := repo.View.SearchApplications(request) - if err != nil { - return nil, err - } - result := &proj_model.ApplicationSearchResponse{ - Offset: request.Offset, - Limit: request.Limit, - TotalResult: count, - Result: model.ApplicationViewsToModel(apps), - } - if sequenceErr == nil { - result.Sequence = sequence.CurrentSequence - result.Timestamp = sequence.LastSuccessfulSpoolerRun - } - return result, nil -} - func (repo *ProjectRepo) ApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ApplicationChanges, error) { changes, err := repo.getApplicationChanges(ctx, projectID, appID, lastSequence, limit, sortAscending, retention) if err != nil { diff --git a/internal/management/repository/eventsourcing/handler/application.go b/internal/management/repository/eventsourcing/handler/application.go deleted file mode 100644 index 7e3ac7a51..000000000 --- a/internal/management/repository/eventsourcing/handler/application.go +++ /dev/null @@ -1,167 +0,0 @@ -package handler - -import ( - "context" - "github.com/caos/zitadel/internal/eventstore/v1" - - "github.com/caos/logging" - - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - proj_model "github.com/caos/zitadel/internal/project/model" - es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model" - proj_view "github.com/caos/zitadel/internal/project/repository/view" - view_model "github.com/caos/zitadel/internal/project/repository/view/model" -) - -const ( - applicationTable = "management.applications" -) - -type Application struct { - handler - subscription *v1.Subscription -} - -func newApplication( - handler handler, -) *Application { - h := &Application{ - handler: handler, - } - - h.subscribe() - - return h -} - -func (a *Application) subscribe() { - a.subscription = a.es.Subscribe(a.AggregateTypes()...) - go func() { - for event := range a.subscription.Events { - query.ReduceEvent(a, event) - } - }() -} - -func (a *Application) ViewModel() string { - return applicationTable -} - -func (a *Application) Subscription() *v1.Subscription { - return a.subscription -} - -func (_ *Application) AggregateTypes() []models.AggregateType { - return []models.AggregateType{es_model.ProjectAggregate} -} - -func (a *Application) CurrentSequence() (uint64, error) { - sequence, err := a.view.GetLatestApplicationSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (a *Application) EventQuery() (*models.SearchQuery, error) { - sequence, err := a.view.GetLatestApplicationSequence() - if err != nil { - return nil, err - } - return proj_view.ProjectQuery(sequence.CurrentSequence), nil -} - -func (a *Application) Reduce(event *models.Event) (err error) { - app := new(view_model.ApplicationView) - switch event.Type { - case es_model.ApplicationAdded: - project, err := a.getProjectByID(context.Background(), event.AggregateID) - if err != nil { - return err - } - app.ProjectRoleCheck = project.ProjectRoleCheck - app.HasProjectCheck = project.HasProjectCheck - app.ProjectRoleAssertion = project.ProjectRoleAssertion - app.PrivateLabelingSetting = project.PrivateLabelingSetting - - err = app.AppendEvent(event) - case es_model.ApplicationChanged, - es_model.OIDCConfigAdded, - es_model.OIDCConfigChanged, - es_model.APIConfigAdded, - es_model.APIConfigChanged, - es_model.ApplicationDeactivated, - es_model.ApplicationReactivated: - err = app.SetData(event) - if err != nil { - return err - } - app, err = a.view.ApplicationByID(event.AggregateID, app.ID) - if err != nil { - return err - } - err = app.AppendEvent(event) - case es_model.ApplicationRemoved: - err = app.SetData(event) - if err != nil { - return err - } - return a.view.DeleteApplication(app.ID, event) - case es_model.ProjectChanged: - apps, err := a.view.ApplicationsByProjectID(event.AggregateID) - if err != nil { - return err - } - if len(apps) == 0 { - return a.view.ProcessedApplicationSequence(event) - } - for _, app := range apps { - if err := app.AppendEvent(event); err != nil { - return err - } - } - return a.view.PutApplications(apps, event) - case es_model.ProjectRemoved: - return a.view.DeleteApplicationsByProjectID(event.AggregateID) - default: - return a.view.ProcessedApplicationSequence(event) - } - if err != nil { - return err - } - return a.view.PutApplication(app, event) -} - -func (a *Application) OnError(event *models.Event, spoolerError error) error { - logging.LogWithFields("SPOOL-ls9ew", "id", event.AggregateID).WithError(spoolerError).Warn("something went wrong in project app handler") - return spooler.HandleError(event, spoolerError, a.view.GetLatestApplicationFailedEvent, a.view.ProcessedApplicationFailedEvent, a.view.ProcessedApplicationSequence, a.errorCountUntilSkip) -} - -func (a *Application) OnSuccess() error { - return spooler.HandleSuccess(a.view.UpdateApplicationSpoolerRunTimestamp) -} - -func (a *Application) getProjectByID(ctx context.Context, projID string) (*proj_model.Project, error) { - query, err := proj_view.ProjectByIDQuery(projID, 0) - if err != nil { - return nil, err - } - esProject := &es_model.Project{ - ObjectRoot: models.ObjectRoot{ - AggregateID: projID, - }, - } - err = es_sdk.Filter(ctx, a.Eventstore().FilterEvents, esProject.AppendEvents, query) - if err != nil && !errors.IsNotFound(err) { - return nil, err - } - if esProject.Sequence == 0 { - return nil, errors.ThrowNotFound(nil, "EVENT-ADvfs", "Errors.Project.NotFound") - } - - return es_model.ProjectToModel(esProject), nil -} diff --git a/internal/management/repository/eventsourcing/handler/handler.go b/internal/management/repository/eventsourcing/handler/handler.go index 22f103b70..130ab93d9 100644 --- a/internal/management/repository/eventsourcing/handler/handler.go +++ b/internal/management/repository/eventsourcing/handler/handler.go @@ -35,7 +35,6 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es return []query.Handler{ newProjectMember(handler{view, bulkLimit, configs.cycleDuration("ProjectMember"), errorCount, es}), newProjectGrantMember(handler{view, bulkLimit, configs.cycleDuration("ProjectGrantMember"), errorCount, es}), - newApplication(handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount, es}), newUser(handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es}, defaults.IamID), newUserGrant(handler{view, bulkLimit, configs.cycleDuration("UserGrant"), errorCount, es}), diff --git a/internal/management/repository/eventsourcing/view/application.go b/internal/management/repository/eventsourcing/view/application.go deleted file mode 100644 index 0508b4a59..000000000 --- a/internal/management/repository/eventsourcing/view/application.go +++ /dev/null @@ -1,74 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - proj_model "github.com/caos/zitadel/internal/project/model" - "github.com/caos/zitadel/internal/project/repository/view" - "github.com/caos/zitadel/internal/project/repository/view/model" - "github.com/caos/zitadel/internal/view/repository" -) - -const ( - applicationTable = "management.applications" -) - -func (v *View) ApplicationByID(projectID, appID string) (*model.ApplicationView, error) { - return view.ApplicationByID(v.Db, applicationTable, projectID, appID) -} - -func (v *View) ApplicationsByProjectID(projectID string) ([]*model.ApplicationView, error) { - return view.ApplicationsByProjectID(v.Db, applicationTable, projectID) -} - -func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, uint64, error) { - return view.SearchApplications(v.Db, applicationTable, request) -} - -func (v *View) PutApplication(app *model.ApplicationView, event *models.Event) error { - err := view.PutApplication(v.Db, applicationTable, app) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedApplicationSequence(event) -} - -func (v *View) PutApplications(apps []*model.ApplicationView, event *models.Event) error { - err := view.PutApplications(v.Db, applicationTable, apps...) - if err != nil { - return err - } - return v.ProcessedApplicationSequence(event) -} - -func (v *View) DeleteApplication(appID string, event *models.Event) error { - err := view.DeleteApplication(v.Db, applicationTable, appID) - if err != nil { - return err - } - return v.ProcessedApplicationSequence(event) -} - -func (v *View) DeleteApplicationsByProjectID(projectID string) error { - return view.DeleteApplicationsByProjectID(v.Db, applicationTable, projectID) -} - -func (v *View) GetLatestApplicationSequence() (*repository.CurrentSequence, error) { - return v.latestSequence(applicationTable) -} - -func (v *View) ProcessedApplicationSequence(event *models.Event) error { - return v.saveCurrentSequence(applicationTable, event) -} - -func (v *View) UpdateApplicationSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(applicationTable) -} - -func (v *View) GetLatestApplicationFailedEvent(sequence uint64) (*repository.FailedEvent, error) { - return v.latestFailedEvent(applicationTable, sequence) -} - -func (v *View) ProcessedApplicationFailedEvent(failedEvent *repository.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/management/repository/project.go b/internal/management/repository/project.go index bf4b94b19..a0f238848 100644 --- a/internal/management/repository/project.go +++ b/internal/management/repository/project.go @@ -17,8 +17,6 @@ type ProjectRepository interface { ProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.ProjectChanges, error) - ApplicationByID(ctx context.Context, projectID, appID string) (*model.ApplicationView, error) - SearchApplications(ctx context.Context, request *model.ApplicationSearchRequest) (*model.ApplicationSearchResponse, error) ApplicationChanges(ctx context.Context, projectID string, appID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.ApplicationChanges, error) SearchClientKeys(ctx context.Context, request *key_model.AuthNKeySearchRequest) (*key_model.AuthNKeySearchResponse, error) GetClientKey(ctx context.Context, projectID, applicationID, keyID string) (*key_model.AuthNKeyView, error) diff --git a/internal/project/repository/view/application_view.go b/internal/project/repository/view/application_view.go deleted file mode 100644 index 92882a606..000000000 --- a/internal/project/repository/view/application_view.go +++ /dev/null @@ -1,89 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" - proj_model "github.com/caos/zitadel/internal/project/model" - "github.com/caos/zitadel/internal/project/repository/view/model" - "github.com/caos/zitadel/internal/view/repository" - "github.com/jinzhu/gorm" -) - -func ApplicationByID(db *gorm.DB, table, projectID, appID string) (*model.ApplicationView, error) { - app := new(model.ApplicationView) - projectIDQuery := &model.ApplicationSearchQuery{Key: proj_model.AppSearchKeyProjectID, Value: projectID, Method: domain.SearchMethodEquals} - appIDQuery := &model.ApplicationSearchQuery{Key: proj_model.AppSearchKeyAppID, Value: appID, Method: domain.SearchMethodEquals} - query := repository.PrepareGetByQuery(table, projectIDQuery, appIDQuery) - err := query(db, app) - if caos_errs.IsNotFound(err) { - return nil, caos_errs.ThrowNotFound(nil, "VIEW-DGdfx", "Errors.Application.NotFound") - } - return app, err -} - -func ApplicationsByProjectID(db *gorm.DB, table, projectID string) ([]*model.ApplicationView, error) { - applications := make([]*model.ApplicationView, 0) - queries := []*proj_model.ApplicationSearchQuery{ - {Key: proj_model.AppSearchKeyProjectID, Value: projectID, Method: domain.SearchMethodEquals}, - } - query := repository.PrepareSearchQuery(table, model.ApplicationSearchRequest{Queries: queries}) - _, err := query(db, &applications) - if err != nil { - return nil, err - } - return applications, nil -} - -func ApplicationByOIDCClientID(db *gorm.DB, table, clientID string) (*model.ApplicationView, error) { - app := new(model.ApplicationView) - clientIDQuery := model.ApplicationSearchQuery{Key: proj_model.AppSearchKeyOIDCClientID, Value: clientID, Method: domain.SearchMethodEquals} - query := repository.PrepareGetByQuery(table, clientIDQuery) - err := query(db, app) - if caos_errs.IsNotFound(err) { - return nil, caos_errs.ThrowNotFound(nil, "VIEW-DG1qh", "Errors.Project.App.NotFound") - } - return app, err -} - -func ApplicationByProjectIDAndAppName(db *gorm.DB, table, projectID, appName string) (*model.ApplicationView, error) { - app := new(model.ApplicationView) - projectIDQuery := model.ApplicationSearchQuery{Key: proj_model.AppSearchKeyProjectID, Value: projectID, Method: domain.SearchMethodEquals} - appNameQuery := model.ApplicationSearchQuery{Key: proj_model.AppSearchKeyName, Value: appName, Method: domain.SearchMethodEquals} - query := repository.PrepareGetByQuery(table, projectIDQuery, appNameQuery) - err := query(db, app) - if caos_errs.IsNotFound(err) { - return nil, caos_errs.ThrowNotFound(nil, "VIEW-Jqw1z", "Errors.Project.App.NotFound") - } - return app, err -} - -func SearchApplications(db *gorm.DB, table string, req *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, uint64, error) { - apps := make([]*model.ApplicationView, 0) - query := repository.PrepareSearchQuery(table, model.ApplicationSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries}) - count, err := query(db, &apps) - return apps, count, err -} - -func PutApplication(db *gorm.DB, table string, app *model.ApplicationView) error { - save := repository.PrepareSave(table) - return save(db, app) -} - -func PutApplications(db *gorm.DB, table string, apps ...*model.ApplicationView) error { - save := repository.PrepareBulkSave(table) - s := make([]interface{}, len(apps)) - for i, app := range apps { - s[i] = app - } - return save(db, s...) -} - -func DeleteApplication(db *gorm.DB, table, appID string) error { - delete := repository.PrepareDeleteByKey(table, model.ApplicationSearchKey(proj_model.AppSearchKeyAppID), appID) - return delete(db) -} - -func DeleteApplicationsByProjectID(db *gorm.DB, table, projectID string) error { - delete := repository.PrepareDeleteByKey(table, model.ApplicationSearchKey(proj_model.AppSearchKeyProjectID), projectID) - return delete(db) -} diff --git a/internal/query/app.go b/internal/query/app.go new file mode 100644 index 000000000..e90073e29 --- /dev/null +++ b/internal/query/app.go @@ -0,0 +1,658 @@ +package query + +import ( + "context" + "database/sql" + errs "errors" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/lib/pq" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query/projection" +) + +type Apps struct { + SearchResponse + Apps []*App +} + +type App struct { + ID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + State domain.AppState + Sequence uint64 + + ProjectID string + Name string + + OIDCConfig *OIDCApp + APIConfig *APIApp +} + +type OIDCApp struct { + RedirectURIs []string + ResponseTypes []domain.OIDCResponseType + GrantTypes []domain.OIDCGrantType + AppType domain.OIDCApplicationType + ClientID string + AuthMethodType domain.OIDCAuthMethodType + PostLogoutRedirectURIs []string + Version domain.OIDCVersion + ComplianceProblems []string + IsDevMode bool + AccessTokenType domain.OIDCTokenType + AssertAccessTokenRole bool + AssertIDTokenRole bool + AssertIDTokenUserinfo bool + ClockSkew time.Duration + AdditionalOrigins []string + AllowedOrigins []string +} + +type APIApp struct { + ClientID string + AuthMethodType domain.APIAuthMethodType +} + +type AppSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *AppSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +var ( + appsTable = table{ + name: projection.AppProjectionTable, + } + AppColumnID = Column{ + name: projection.AppColumnID, + table: appsTable, + } + AppColumnName = Column{ + name: projection.AppColumnName, + table: appsTable, + } + AppColumnProjectID = Column{ + name: projection.AppColumnProjectID, + table: appsTable, + } + AppColumnCreationDate = Column{ + name: projection.AppColumnCreationDate, + table: appsTable, + } + AppColumnChangeDate = Column{ + name: projection.AppColumnChangeDate, + table: appsTable, + } + AppColumnResourceOwner = Column{ + name: projection.AppColumnResourceOwner, + table: appsTable, + } + AppColumnState = Column{ + name: projection.AppColumnState, + table: appsTable, + } + AppColumnSequence = Column{ + name: projection.AppColumnSequence, + table: appsTable, + } +) + +var ( + appAPIConfigsTable = table{ + name: projection.AppAPITable, + } + AppAPIConfigColumnAppID = Column{ + name: projection.AppAPIConfigColumnAppID, + table: appAPIConfigsTable, + } + AppAPIConfigColumnClientID = Column{ + name: projection.AppAPIConfigColumnClientID, + table: appAPIConfigsTable, + } + AppAPIConfigColumnAuthMethod = Column{ + name: projection.AppAPIConfigColumnAuthMethod, + table: appAPIConfigsTable, + } +) + +var ( + appOIDCConfigsTable = table{ + name: projection.AppOIDCTable, + } + AppOIDCConfigColumnAppID = Column{ + name: projection.AppOIDCConfigColumnAppID, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnVersion = Column{ + name: projection.AppOIDCConfigColumnVersion, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnClientID = Column{ + name: projection.AppOIDCConfigColumnClientID, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnRedirectUris = Column{ + name: projection.AppOIDCConfigColumnRedirectUris, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnResponseTypes = Column{ + name: projection.AppOIDCConfigColumnResponseTypes, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnGrantTypes = Column{ + name: projection.AppOIDCConfigColumnGrantTypes, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnApplicationType = Column{ + name: projection.AppOIDCConfigColumnApplicationType, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnAuthMethodType = Column{ + name: projection.AppOIDCConfigColumnAuthMethodType, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnPostLogoutRedirectUris = Column{ + name: projection.AppOIDCConfigColumnPostLogoutRedirectUris, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnDevMode = Column{ + name: projection.AppOIDCConfigColumnDevMode, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnAccessTokenType = Column{ + name: projection.AppOIDCConfigColumnAccessTokenType, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnAccessTokenRoleAssertion = Column{ + name: projection.AppOIDCConfigColumnAccessTokenRoleAssertion, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnIDTokenRoleAssertion = Column{ + name: projection.AppOIDCConfigColumnIDTokenRoleAssertion, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnIDTokenUserinfoAssertion = Column{ + name: projection.AppOIDCConfigColumnIDTokenUserinfoAssertion, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnClockSkew = Column{ + name: projection.AppOIDCConfigColumnClockSkew, + table: appOIDCConfigsTable, + } + AppOIDCConfigColumnAdditionalOrigins = Column{ + name: projection.AppOIDCConfigColumnAdditionalOrigins, + table: appOIDCConfigsTable, + } +) + +func (q *Queries) AppByProjectAndAppID(ctx context.Context, projectID, appID string) (*App, error) { + stmt, scan := prepareAppQuery() + query, args, err := stmt.Where( + sq.Eq{ + AppColumnID.identifier(): appID, + AppColumnProjectID.identifier(): projectID, + }, + ).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-AFDgg", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func (q *Queries) AppByID(ctx context.Context, appID string) (*App, error) { + stmt, scan := prepareAppQuery() + query, args, err := stmt.Where( + sq.Eq{AppColumnID.identifier(): appID}, + ).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-immt9", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func (q *Queries) ProjectIDFromOIDCClientID(ctx context.Context, appID string) (string, error) { + stmt, scan := prepareProjectIDByAppQuery() + query, args, err := stmt.Where( + sq.Eq{AppOIDCConfigColumnClientID.identifier(): appID}, + ).ToSql() + if err != nil { + return "", errors.ThrowInternal(err, "QUERY-7d92U", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func (q *Queries) ProjectByOIDCClientID(ctx context.Context, id string) (*Project, error) { + stmt, scan := prepareProjectByAppQuery() + query, args, err := stmt.Where( + sq.Eq{AppOIDCConfigColumnClientID.identifier(): id}, + ).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-XhJi4", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func (q *Queries) AppByOIDCClientID(ctx context.Context, clientID string) (*App, error) { + stmt, scan := prepareAppQuery() + query, args, err := stmt.Where( + sq.Eq{ + AppOIDCConfigColumnClientID.identifier(): clientID, + }, + ).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-JgVop", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries) (*Apps, error) { + query, scan := prepareAppsQuery() + stmt, args, err := queries.toQuery(query).ToSql() + if err != nil { + return nil, errors.ThrowInvalidArgument(err, "QUERY-fajp8", "Errors.Query.InvalidRequest") + } + + rows, err := q.client.QueryContext(ctx, stmt, args...) + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-aJnZL", "Errors.Internal") + } + apps, err := scan(rows) + if err != nil { + return nil, err + } + apps.LatestSequence, err = q.latestSequence(ctx, appsTable) + return apps, err +} + +func (q *Queries) SearchAppIDs(ctx context.Context, queries *AppSearchQueries) ([]string, error) { + query, scan := prepareAppIDsQuery() + stmt, args, err := queries.toQuery(query).ToSql() + if err != nil { + return nil, errors.ThrowInvalidArgument(err, "QUERY-fajp8", "Errors.Query.InvalidRequest") + } + + rows, err := q.client.QueryContext(ctx, stmt, args...) + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-aJnZL", "Errors.Internal") + } + return scan(rows) +} + +func NewAppNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(AppColumnName, value, method) +} + +func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) { + return NewTextQuery(AppColumnProjectID, id, TextEquals) +} + +func prepareAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) { + return sq.Select( + AppColumnID.identifier(), + AppColumnName.identifier(), + AppColumnProjectID.identifier(), + AppColumnCreationDate.identifier(), + AppColumnChangeDate.identifier(), + AppColumnResourceOwner.identifier(), + AppColumnState.identifier(), + AppColumnSequence.identifier(), + + AppAPIConfigColumnAppID.identifier(), + AppAPIConfigColumnClientID.identifier(), + AppAPIConfigColumnAuthMethod.identifier(), + + AppOIDCConfigColumnAppID.identifier(), + AppOIDCConfigColumnVersion.identifier(), + AppOIDCConfigColumnClientID.identifier(), + AppOIDCConfigColumnRedirectUris.identifier(), + AppOIDCConfigColumnResponseTypes.identifier(), + AppOIDCConfigColumnGrantTypes.identifier(), + AppOIDCConfigColumnApplicationType.identifier(), + AppOIDCConfigColumnAuthMethodType.identifier(), + AppOIDCConfigColumnPostLogoutRedirectUris.identifier(), + AppOIDCConfigColumnDevMode.identifier(), + AppOIDCConfigColumnAccessTokenType.identifier(), + AppOIDCConfigColumnAccessTokenRoleAssertion.identifier(), + AppOIDCConfigColumnIDTokenRoleAssertion.identifier(), + AppOIDCConfigColumnIDTokenUserinfoAssertion.identifier(), + AppOIDCConfigColumnClockSkew.identifier(), + AppOIDCConfigColumnAdditionalOrigins.identifier(), + ).From(appsTable.identifier()). + LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). + LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). + PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*App, error) { + app := new(App) + + var ( + apiConfig = sqlAPIConfig{} + oidcConfig = sqlOIDCConfig{} + ) + + err := row.Scan( + &app.ID, + &app.Name, + &app.ProjectID, + &app.CreationDate, + &app.ChangeDate, + &app.ResourceOwner, + &app.State, + &app.Sequence, + + &apiConfig.appID, + &apiConfig.clientID, + &apiConfig.authMethod, + + &oidcConfig.appID, + &oidcConfig.version, + &oidcConfig.clientID, + &oidcConfig.redirectUris, + &oidcConfig.responseTypes, + &oidcConfig.grantTypes, + &oidcConfig.applicationType, + &oidcConfig.authMethodType, + &oidcConfig.postLogoutRedirectUris, + &oidcConfig.devMode, + &oidcConfig.accessTokenType, + &oidcConfig.accessTokenRoleAssertion, + &oidcConfig.iDTokenRoleAssertion, + &oidcConfig.iDTokenUserinfoAssertion, + &oidcConfig.clockSkew, + &oidcConfig.additionalOrigins, + ) + + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-pCP8P", "Errors.App.NotExisting") + } + return nil, errors.ThrowInternal(err, "QUERY-0R2Nw", "Errors.Internal") + } + + apiConfig.set(app) + oidcConfig.set(app) + + return app, nil + } +} + +func prepareProjectIDByAppQuery() (sq.SelectBuilder, func(*sql.Row) (projectID string, err error)) { + return sq.Select( + AppColumnProjectID.identifier(), + ).From(appsTable.identifier()). + LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). + LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). + PlaceholderFormat(sq.Dollar), func(row *sql.Row) (projectID string, err error) { + err = row.Scan( + &projectID, + ) + + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return "", errors.ThrowNotFound(err, "QUERY-aKcc2", "Errors.Project.NotExisting") + } + return "", errors.ThrowInternal(err, "QUERY-3A5TG", "Errors.Internal") + } + + return projectID, nil + } +} + +func prepareProjectByAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { + return sq.Select( + ProjectColumnID.identifier(), + ProjectColumnCreationDate.identifier(), + ProjectColumnChangeDate.identifier(), + ProjectColumnResourceOwner.identifier(), + ProjectColumnState.identifier(), + ProjectColumnSequence.identifier(), + ProjectColumnName.identifier(), + ProjectColumnProjectRoleAssertion.identifier(), + ProjectColumnProjectRoleCheck.identifier(), + ProjectColumnHasProjectCheck.identifier(), + ProjectColumnPrivateLabelingSetting.identifier(), + ).From(projectsTable.identifier()). + Join(join(AppColumnProjectID, ProjectColumnID)). + LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). + LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Project, error) { + p := new(Project) + err := row.Scan( + &p.ID, + &p.CreationDate, + &p.ChangeDate, + &p.ResourceOwner, + &p.State, + &p.Sequence, + &p.Name, + &p.ProjectRoleAssertion, + &p.ProjectRoleCheck, + &p.HasProjectCheck, + &p.PrivateLabelingSetting, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-fk2fs", "Errors.Project.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-dj2FF", "Errors.Internal") + } + return p, nil + } +} + +func prepareAppsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) { + return sq.Select( + AppColumnID.identifier(), + AppColumnName.identifier(), + AppColumnProjectID.identifier(), + AppColumnCreationDate.identifier(), + AppColumnChangeDate.identifier(), + AppColumnResourceOwner.identifier(), + AppColumnState.identifier(), + AppColumnSequence.identifier(), + + AppAPIConfigColumnAppID.identifier(), + AppAPIConfigColumnClientID.identifier(), + AppAPIConfigColumnAuthMethod.identifier(), + + AppOIDCConfigColumnAppID.identifier(), + AppOIDCConfigColumnVersion.identifier(), + AppOIDCConfigColumnClientID.identifier(), + AppOIDCConfigColumnRedirectUris.identifier(), + AppOIDCConfigColumnResponseTypes.identifier(), + AppOIDCConfigColumnGrantTypes.identifier(), + AppOIDCConfigColumnApplicationType.identifier(), + AppOIDCConfigColumnAuthMethodType.identifier(), + AppOIDCConfigColumnPostLogoutRedirectUris.identifier(), + AppOIDCConfigColumnDevMode.identifier(), + AppOIDCConfigColumnAccessTokenType.identifier(), + AppOIDCConfigColumnAccessTokenRoleAssertion.identifier(), + AppOIDCConfigColumnIDTokenRoleAssertion.identifier(), + AppOIDCConfigColumnIDTokenUserinfoAssertion.identifier(), + AppOIDCConfigColumnClockSkew.identifier(), + AppOIDCConfigColumnAdditionalOrigins.identifier(), + countColumn.identifier(), + ).From(appsTable.identifier()). + LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). + LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). + PlaceholderFormat(sq.Dollar), func(row *sql.Rows) (*Apps, error) { + apps := &Apps{Apps: []*App{}} + + for row.Next() { + app := new(App) + var ( + apiConfig = sqlAPIConfig{} + oidcConfig = sqlOIDCConfig{} + ) + + err := row.Scan( + &app.ID, + &app.Name, + &app.ProjectID, + &app.CreationDate, + &app.ChangeDate, + &app.ResourceOwner, + &app.State, + &app.Sequence, + + &apiConfig.appID, + &apiConfig.clientID, + &apiConfig.authMethod, + + &oidcConfig.appID, + &oidcConfig.version, + &oidcConfig.clientID, + &oidcConfig.redirectUris, + &oidcConfig.responseTypes, + &oidcConfig.grantTypes, + &oidcConfig.applicationType, + &oidcConfig.authMethodType, + &oidcConfig.postLogoutRedirectUris, + &oidcConfig.devMode, + &oidcConfig.accessTokenType, + &oidcConfig.accessTokenRoleAssertion, + &oidcConfig.iDTokenRoleAssertion, + &oidcConfig.iDTokenUserinfoAssertion, + &oidcConfig.clockSkew, + &oidcConfig.additionalOrigins, + &apps.Count, + ) + + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-0R2Nw", "Errors.Internal") + } + + apiConfig.set(app) + oidcConfig.set(app) + + apps.Apps = append(apps.Apps, app) + } + + return apps, nil + } +} + +func prepareAppIDsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]string, error)) { + return sq.Select( + AppColumnID.identifier(), + ).From(appsTable.identifier()). + LeftJoin(join(AppAPIConfigColumnAppID, AppColumnID)). + LeftJoin(join(AppOIDCConfigColumnAppID, AppColumnID)). + PlaceholderFormat(sq.Dollar), func(row *sql.Rows) ([]string, error) { + ids := []string{} + + for row.Next() { + var id string + if err := row.Scan(&id); err != nil { + return nil, errors.ThrowInternal(err, "QUERY-0R2Nw", "Errors.Internal") + } + + ids = append(ids, id) + } + + return ids, nil + } +} + +type sqlOIDCConfig struct { + appID sql.NullString + version sql.NullInt32 + clientID sql.NullString + redirectUris pq.StringArray + applicationType sql.NullInt16 + authMethodType sql.NullInt16 + postLogoutRedirectUris pq.StringArray + devMode sql.NullBool + accessTokenType sql.NullInt16 + accessTokenRoleAssertion sql.NullBool + iDTokenRoleAssertion sql.NullBool + iDTokenUserinfoAssertion sql.NullBool + clockSkew sql.NullInt64 + additionalOrigins pq.StringArray + responseTypes pq.Int32Array + grantTypes pq.Int32Array +} + +func (c sqlOIDCConfig) set(app *App) { + if !c.appID.Valid { + return + } + app.OIDCConfig = &OIDCApp{ + Version: domain.OIDCVersion(c.version.Int32), + ClientID: c.clientID.String, + RedirectURIs: c.redirectUris, + AppType: domain.OIDCApplicationType(c.applicationType.Int16), + AuthMethodType: domain.OIDCAuthMethodType(c.authMethodType.Int16), + PostLogoutRedirectURIs: c.postLogoutRedirectUris, + IsDevMode: c.devMode.Bool, + AccessTokenType: domain.OIDCTokenType(c.accessTokenType.Int16), + AssertAccessTokenRole: c.accessTokenRoleAssertion.Bool, + AssertIDTokenRole: c.iDTokenRoleAssertion.Bool, + AssertIDTokenUserinfo: c.iDTokenUserinfoAssertion.Bool, + ClockSkew: time.Duration(c.clockSkew.Int64), + AdditionalOrigins: c.additionalOrigins, + ResponseTypes: oidcResponseTypesToDomain(c.responseTypes), + GrantTypes: oidcGrantTypesToDomain(c.grantTypes), + } + compliance := domain.GetOIDCCompliance(app.OIDCConfig.Version, app.OIDCConfig.AppType, app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, app.OIDCConfig.AuthMethodType, app.OIDCConfig.RedirectURIs) + app.OIDCConfig.ComplianceProblems = compliance.Problems + + var err error + app.OIDCConfig.AllowedOrigins, err = domain.OIDCOriginAllowList(app.OIDCConfig.RedirectURIs, app.OIDCConfig.AdditionalOrigins) + logging.LogWithFields("app", app.ID).OnError(err).Warn("unable to set allowed origins") +} + +type sqlAPIConfig struct { + appID sql.NullString + clientID sql.NullString + authMethod sql.NullInt16 +} + +func (c sqlAPIConfig) set(app *App) { + if !c.appID.Valid { + return + } + app.APIConfig = &APIApp{ + ClientID: c.clientID.String, + AuthMethodType: domain.APIAuthMethodType(c.authMethod.Int16), + } +} + +func oidcResponseTypesToDomain(t pq.Int32Array) []domain.OIDCResponseType { + types := make([]domain.OIDCResponseType, len(t)) + for i, typ := range t { + types[i] = domain.OIDCResponseType(typ) + } + return types +} + +func oidcGrantTypesToDomain(t pq.Int32Array) []domain.OIDCGrantType { + types := make([]domain.OIDCGrantType, len(t)) + for i, typ := range t { + types[i] = domain.OIDCGrantType(typ) + } + return types +} diff --git a/internal/query/app_test.go b/internal/query/app_test.go new file mode 100644 index 000000000..b1294cd85 --- /dev/null +++ b/internal/query/app_test.go @@ -0,0 +1,1699 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + "time" + + "github.com/caos/zitadel/internal/domain" + errs "github.com/caos/zitadel/internal/errors" + "github.com/lib/pq" +) + +var ( + expectedAppQuery = regexp.QuoteMeta(`SELECT zitadel.projections.apps.id,` + + ` zitadel.projections.apps.name,` + + ` zitadel.projections.apps.project_id,` + + ` zitadel.projections.apps.creation_date,` + + ` zitadel.projections.apps.change_date,` + + ` zitadel.projections.apps.resource_owner,` + + ` zitadel.projections.apps.state,` + + ` zitadel.projections.apps.sequence,` + + // api config + ` zitadel.projections.apps_api_configs.app_id,` + + ` zitadel.projections.apps_api_configs.client_id,` + + ` zitadel.projections.apps_api_configs.auth_method,` + + // oidc config + ` zitadel.projections.apps_oidc_configs.app_id,` + + ` zitadel.projections.apps_oidc_configs.version,` + + ` zitadel.projections.apps_oidc_configs.client_id,` + + ` zitadel.projections.apps_oidc_configs.redirect_uris,` + + ` zitadel.projections.apps_oidc_configs.response_types,` + + ` zitadel.projections.apps_oidc_configs.grant_types,` + + ` zitadel.projections.apps_oidc_configs.application_type,` + + ` zitadel.projections.apps_oidc_configs.auth_method_type,` + + ` zitadel.projections.apps_oidc_configs.post_logout_redirect_uris,` + + ` zitadel.projections.apps_oidc_configs.is_dev_mode,` + + ` zitadel.projections.apps_oidc_configs.access_token_type,` + + ` zitadel.projections.apps_oidc_configs.access_token_role_assertion,` + + ` zitadel.projections.apps_oidc_configs.id_token_role_assertion,` + + ` zitadel.projections.apps_oidc_configs.id_token_userinfo_assertion,` + + ` zitadel.projections.apps_oidc_configs.clock_skew,` + + ` zitadel.projections.apps_oidc_configs.additional_origins` + + ` FROM zitadel.projections.apps` + + ` LEFT JOIN zitadel.projections.apps_api_configs ON zitadel.projections.apps.id = zitadel.projections.apps_api_configs.app_id` + + ` LEFT JOIN zitadel.projections.apps_oidc_configs ON zitadel.projections.apps.id = zitadel.projections.apps_oidc_configs.app_id`) + expectedAppsQuery = regexp.QuoteMeta(`SELECT zitadel.projections.apps.id,` + + ` zitadel.projections.apps.name,` + + ` zitadel.projections.apps.project_id,` + + ` zitadel.projections.apps.creation_date,` + + ` zitadel.projections.apps.change_date,` + + ` zitadel.projections.apps.resource_owner,` + + ` zitadel.projections.apps.state,` + + ` zitadel.projections.apps.sequence,` + + // api config + ` zitadel.projections.apps_api_configs.app_id,` + + ` zitadel.projections.apps_api_configs.client_id,` + + ` zitadel.projections.apps_api_configs.auth_method,` + + // oidc config + ` zitadel.projections.apps_oidc_configs.app_id,` + + ` zitadel.projections.apps_oidc_configs.version,` + + ` zitadel.projections.apps_oidc_configs.client_id,` + + ` zitadel.projections.apps_oidc_configs.redirect_uris,` + + ` zitadel.projections.apps_oidc_configs.response_types,` + + ` zitadel.projections.apps_oidc_configs.grant_types,` + + ` zitadel.projections.apps_oidc_configs.application_type,` + + ` zitadel.projections.apps_oidc_configs.auth_method_type,` + + ` zitadel.projections.apps_oidc_configs.post_logout_redirect_uris,` + + ` zitadel.projections.apps_oidc_configs.is_dev_mode,` + + ` zitadel.projections.apps_oidc_configs.access_token_type,` + + ` zitadel.projections.apps_oidc_configs.access_token_role_assertion,` + + ` zitadel.projections.apps_oidc_configs.id_token_role_assertion,` + + ` zitadel.projections.apps_oidc_configs.id_token_userinfo_assertion,` + + ` zitadel.projections.apps_oidc_configs.clock_skew,` + + ` zitadel.projections.apps_oidc_configs.additional_origins,` + + ` COUNT(*) OVER ()` + + ` FROM zitadel.projections.apps` + + ` LEFT JOIN zitadel.projections.apps_api_configs ON zitadel.projections.apps.id = zitadel.projections.apps_api_configs.app_id` + + ` LEFT JOIN zitadel.projections.apps_oidc_configs ON zitadel.projections.apps.id = zitadel.projections.apps_oidc_configs.app_id`) + expectedAppIDsQuery = regexp.QuoteMeta(`SELECT zitadel.projections.apps.id` + + ` FROM zitadel.projections.apps` + + ` LEFT JOIN zitadel.projections.apps_api_configs ON zitadel.projections.apps.id = zitadel.projections.apps_api_configs.app_id` + + ` LEFT JOIN zitadel.projections.apps_oidc_configs ON zitadel.projections.apps.id = zitadel.projections.apps_oidc_configs.app_id`) + expectedProjectIDByAppQuery = regexp.QuoteMeta(`SELECT zitadel.projections.apps.project_id` + + ` FROM zitadel.projections.apps` + + ` LEFT JOIN zitadel.projections.apps_api_configs ON zitadel.projections.apps.id = zitadel.projections.apps_api_configs.app_id` + + ` LEFT JOIN zitadel.projections.apps_oidc_configs ON zitadel.projections.apps.id = zitadel.projections.apps_oidc_configs.app_id`) + expectedProjectByAppQuery = regexp.QuoteMeta(`SELECT zitadel.projections.projects.id,` + + ` zitadel.projections.projects.creation_date,` + + ` zitadel.projections.projects.change_date,` + + ` zitadel.projections.projects.resource_owner,` + + ` zitadel.projections.projects.state,` + + ` zitadel.projections.projects.sequence,` + + ` zitadel.projections.projects.name,` + + ` zitadel.projections.projects.project_role_assertion,` + + ` zitadel.projections.projects.project_role_check,` + + ` zitadel.projections.projects.has_project_check,` + + ` zitadel.projections.projects.private_labeling_setting` + + ` FROM zitadel.projections.projects` + + ` JOIN zitadel.projections.apps ON zitadel.projections.projects.id = zitadel.projections.apps.project_id` + + ` LEFT JOIN zitadel.projections.apps_api_configs ON zitadel.projections.apps.id = zitadel.projections.apps_api_configs.app_id` + + ` LEFT JOIN zitadel.projections.apps_oidc_configs ON zitadel.projections.apps.id = zitadel.projections.apps_oidc_configs.app_id`) + + appCols = []string{ + "id", + "name", + "project_id", + "creation_date", + "change_date", + "resource_owner", + "state", + "sequence", + // api config + "app_id", + "client_id", + "auth_method", + // oidc config + "app_id", + "version", + "client_id", + "redirect_uris", + "response_types", + "grant_types", + "application_type", + "auth_method_type", + "post_logout_redirect_uris", + "is_dev_mode", + "access_token_type", + "access_token_role_assertion", + "id_token_role_assertion", + "id_token_userinfo_assertion", + "clock_skew", + "additional_origins", + } + appsCols = append(appCols, "count") +) + +func Test_AppsPrepare(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareAppsQuery no result", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + nil, + nil, + ), + }, + object: &Apps{Apps: []*App{}}, + }, + { + name: "prepareAppsQuery only app", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + appsCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + }, + }, + ), + }, + object: &Apps{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Apps: []*App{ + { + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + }, + }, + }, + }, + { + name: "prepareAppsQuery api app", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + appsCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + "app-id", + "api-client-id", + domain.APIAuthMethodTypePrivateKeyJWT, + // oidc config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + }, + }, + ), + }, + object: &Apps{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Apps: []*App{ + { + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + APIConfig: &APIApp{ + ClientID: "api-client-id", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + }, + }, + }, + }, + { + name: "prepareAppsQuery oidc app", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + appsCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + true, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &Apps{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Apps: []*App{ + { + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + }, + }, + { + name: "prepareAppsQuery oidc app AssertIDTokenUserinfo active", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + appsCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + false, + domain.OIDCTokenTypeJWT, + false, + false, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &Apps{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Apps: []*App{ + { + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: false, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: false, + AssertIDTokenRole: false, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + }, + }, + { + name: "prepareAppsQuery oidc app AssertIDTokenRole active", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + appsCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + false, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &Apps{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Apps: []*App{ + { + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: false, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + }, + }, + { + name: "prepareAppsQuery oidc app AssertAccessTokenRole active", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + appsCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + false, + domain.OIDCTokenTypeJWT, + false, + true, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &Apps{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Apps: []*App{ + { + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: false, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: false, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + }, + }, + { + name: "prepareAppsQuery oidc app IsDevMode active", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + appsCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + false, + domain.OIDCTokenTypeJWT, + true, + true, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &Apps{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Apps: []*App{ + { + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: false, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + }, + }, + { + name: "prepareAppsQuery multiple result", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppsQuery, + appsCols, + [][]driver.Value{ + { + "oidc-app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "oidc-app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + true, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + { + "api-app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + "api-app-id", + "api-client-id", + domain.APIAuthMethodTypePrivateKeyJWT, + // oidc config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + }, + }, + ), + }, + object: &Apps{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Apps: []*App{ + { + ID: "oidc-app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + { + ID: "api-app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + APIConfig: &APIApp{ + ClientID: "api-client-id", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + }, + }, + }, + }, + { + name: "prepareAppsQuery sql err", + prepare: prepareAppsQuery, + want: want{ + sqlExpectations: mockQueryErr( + expectedAppsQuery, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} + +func Test_AppPrepare(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareAppQuery no result", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppQuery, + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*App)(nil), + }, + { + name: "prepareAppQuery found", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQuery( + expectedAppQuery, + appCols, + []driver.Value{ + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + }, + ), + }, + object: &App{ + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + }, + }, + { + name: "prepareAppQuery api app", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppQuery, + appCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + "app-id", + "api-client-id", + domain.APIAuthMethodTypePrivateKeyJWT, + // oidc config + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + }, + }, + ), + }, + object: &App{ + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + APIConfig: &APIApp{ + ClientID: "api-client-id", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + }, + }, + { + name: "prepareAppQuery oidc app", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppQuery, + appCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + true, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &App{ + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + { + name: "prepareAppQuery oidc app IsDevMode inactive", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppQuery, + appCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + false, + domain.OIDCTokenTypeJWT, + true, + true, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &App{ + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: false, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + { + name: "prepareAppQuery oidc app AssertAccessTokenRole inactive", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppQuery, + appCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + true, + domain.OIDCTokenTypeJWT, + false, + true, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &App{ + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: false, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + { + name: "prepareAppQuery oidc app AssertIDTokenRole inactive", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppQuery, + appCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + false, + true, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &App{ + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: false, + AssertIDTokenUserinfo: true, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + { + name: "prepareAppQuery oidc app AssertIDTokenUserinfo inactive", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppQuery, + appCols, + [][]driver.Value{ + { + "app-id", + "app-name", + "project-id", + testNow, + testNow, + "ro", + domain.AppStateActive, + uint64(20211109), + // api config + nil, + nil, + nil, + // oidc config + "app-id", + domain.OIDCVersionV1, + "oidc-client-id", + pq.StringArray{"https://redirect.to/me"}, + pq.Int32Array{int32(domain.OIDCResponseTypeIDTokenToken)}, + pq.Int32Array{int32(domain.OIDCGrantTypeImplicit)}, + domain.OIDCApplicationTypeUserAgent, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"post.logout.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + true, + false, + 1 * time.Second, + pq.StringArray{"additional.origin"}, + }, + }, + ), + }, + object: &App{ + ID: "app-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.AppStateActive, + Sequence: 20211109, + Name: "app-name", + ProjectID: "project-id", + OIDCConfig: &OIDCApp{ + Version: domain.OIDCVersionV1, + ClientID: "oidc-client-id", + RedirectURIs: []string{"https://redirect.to/me"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeIDTokenToken}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeImplicit}, + AppType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: []string{"post.logout.ch"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: false, + ClockSkew: 1 * time.Second, + AdditionalOrigins: []string{"additional.origin"}, + ComplianceProblems: nil, + AllowedOrigins: []string{"https://redirect.to", "additional.origin"}, + }, + }, + }, + { + name: "prepareAppQuery sql err", + prepare: prepareAppQuery, + want: want{ + sqlExpectations: mockQueryErr( + expectedAppQuery, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} + +func Test_AppIDsPrepare(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareAppIDsQuery no result", + prepare: prepareAppIDsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppIDsQuery, + nil, + nil, + ), + }, + object: []string{}, + }, + { + name: "prepareAppIDsQuery one result", + prepare: prepareAppIDsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppIDsQuery, + []string{"id"}, + [][]driver.Value{ + { + "app-id", + }, + }, + ), + }, + object: []string{"app-id"}, + }, + { + name: "prepareAppIDsQuery multiple result", + prepare: prepareAppIDsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedAppIDsQuery, + []string{"id"}, + [][]driver.Value{ + { + "oidc-app-id", + }, + { + "api-app-id", + }, + }, + ), + }, + object: []string{"oidc-app-id", "api-app-id"}, + }, + { + name: "prepareAppIDsQuery sql err", + prepare: prepareAppIDsQuery, + want: want{ + sqlExpectations: mockQueryErr( + expectedAppIDsQuery, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} + +func Test_ProjectIDByAppPrepare(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareProjectIDByAppQuery no result", + prepare: prepareProjectIDByAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedProjectIDByAppQuery, + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: "", + }, + { + name: "prepareProjectIDByAppQuery one result", + prepare: prepareProjectIDByAppQuery, + want: want{ + sqlExpectations: mockQuery( + expectedProjectIDByAppQuery, + []string{"project_id"}, + []driver.Value{"project-id"}, + ), + }, + object: "project-id", + }, + { + name: "prepareProjectIDByAppQuery sql err", + prepare: prepareProjectIDByAppQuery, + want: want{ + sqlExpectations: mockQueryErr( + expectedProjectIDByAppQuery, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} + +func Test_ProjectByAppPrepare(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareProjectByAppQuery no result", + prepare: prepareProjectByAppQuery, + want: want{ + sqlExpectations: mockQueries( + expectedProjectByAppQuery, + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*Project)(nil), + }, + { + name: "prepareProjectByAppQuery found", + prepare: prepareProjectByAppQuery, + want: want{ + sqlExpectations: mockQuery( + expectedProjectByAppQuery, + projectCols, + []driver.Value{ + "project-id", + testNow, + testNow, + "ro", + domain.ProjectStateInactive, + uint64(20211109), + "project-name", + true, + true, + true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + ), + }, + object: &Project{ + ID: "project-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + Name: "project-name", + State: domain.ProjectStateInactive, + ProjectRoleAssertion: true, + ProjectRoleCheck: true, + HasProjectCheck: true, + PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + }, + { + name: "prepareProjectByAppQuery found", + prepare: prepareProjectByAppQuery, + want: want{ + sqlExpectations: mockQuery( + expectedProjectByAppQuery, + projectCols, + []driver.Value{ + "project-id", + testNow, + testNow, + "ro", + domain.ProjectStateInactive, + uint64(20211109), + "project-name", + false, + true, + true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + ), + }, + object: &Project{ + ID: "project-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + Name: "project-name", + State: domain.ProjectStateInactive, + ProjectRoleAssertion: false, + ProjectRoleCheck: true, + HasProjectCheck: true, + PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + }, + { + name: "prepareProjectByAppQuery found", + prepare: prepareProjectByAppQuery, + want: want{ + sqlExpectations: mockQuery( + expectedProjectByAppQuery, + projectCols, + []driver.Value{ + "project-id", + testNow, + testNow, + "ro", + domain.ProjectStateInactive, + uint64(20211109), + "project-name", + true, + false, + true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + ), + }, + object: &Project{ + ID: "project-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + Name: "project-name", + State: domain.ProjectStateInactive, + ProjectRoleAssertion: true, + ProjectRoleCheck: false, + HasProjectCheck: true, + PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + }, + { + name: "prepareProjectByAppQuery found", + prepare: prepareProjectByAppQuery, + want: want{ + sqlExpectations: mockQuery( + expectedProjectByAppQuery, + projectCols, + []driver.Value{ + "project-id", + testNow, + testNow, + "ro", + domain.ProjectStateInactive, + uint64(20211109), + "project-name", + true, + true, + false, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + ), + }, + object: &Project{ + ID: "project-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + Name: "project-name", + State: domain.ProjectStateInactive, + ProjectRoleAssertion: true, + ProjectRoleCheck: true, + HasProjectCheck: false, + PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + }, + { + name: "prepareProjectByAppQuery sql err", + prepare: prepareProjectByAppQuery, + want: want{ + sqlExpectations: mockQueryErr( + expectedProjectByAppQuery, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/query/project_test.go b/internal/query/project_test.go index 94810c983..f7ef3a5e9 100644 --- a/internal/query/project_test.go +++ b/internal/query/project_test.go @@ -12,6 +12,22 @@ import ( errs "github.com/caos/zitadel/internal/errors" ) +var ( + projectCols = []string{ + "id", + "creation_date", + "change_date", + "resource_owner", + "state", + "sequence", + "name", + "project_role_assertion", + "project_role_check", + "has_project_check", + "private_labeling_setting", + } +) + func Test_ProjectPrepares(t *testing.T) { type want struct { sqlExpectations sqlExpectation diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 61eb74a49..cc4262217 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -815,6 +815,7 @@ EventTypes: removed: Aktion gelöscht Application: OIDC: + UnsupportedVersion: Deine OIDC Version wird nicht unterstützt V1: NotCompliant: Deine Konfiguration ist nicht konform und weicht vom OIDC 1.0 Standard ab. NoRedirectUris: Es muss mindestens eine Redirect URI erfasst sein. diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 089a8821b..a53213d7e 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -812,6 +812,7 @@ EventTypes: removed: Action removed Application: OIDC: + UnsupportedVersion: Your OIDC version is not supported V1: NotCompliant: Your configuration is not compliant and differs from OIDC 1.0 standard. NoRedirectUris: At least one redirect uri must be registered. diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index b577fae93..630f63592 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -810,6 +810,7 @@ EventTypes: removed: Azione rimossa Application: OIDC: + UnsupportedVersion: La tua versione di OIDC non è supportata V1: NotCompliant: La tua configurazione non è conforme e differisce dallo standard OIDC 1.0. NoRedirectUris: Deve essere registrato almeno un URI di reindirizzamento.