diff --git a/internal/query/projection/app.go b/internal/query/projection/app.go new file mode 100644 index 0000000000..d91186bbc9 --- /dev/null +++ b/internal/query/projection/app.go @@ -0,0 +1,456 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/project" + "github.com/lib/pq" +) + +type AppProjection struct { + crdb.StatementHandler +} + +const ( + AppProjectionTable = "zitadel.projections.apps" + AppAPITable = AppProjectionTable + "_" + appAPITableSuffix + AppOIDCTable = AppProjectionTable + "_" + appOIDCTableSuffix +) + +func NewAppProjection(ctx context.Context, config crdb.StatementHandlerConfig) *AppProjection { + p := &AppProjection{} + config.ProjectionName = AppProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *AppProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: project.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: project.ApplicationAddedType, + Reduce: p.reduceAppAdded, + }, + { + Event: project.ApplicationChangedType, + Reduce: p.reduceAppChanged, + }, + { + Event: project.ApplicationDeactivatedType, + Reduce: p.reduceAppDeactivated, + }, + { + Event: project.ApplicationReactivatedType, + Reduce: p.reduceAppReactivated, + }, + { + Event: project.ApplicationRemovedType, + Reduce: p.reduceAppRemoved, + }, + { + Event: project.APIConfigAddedType, + Reduce: p.reduceAPIConfigAdded, + }, + { + Event: project.APIConfigChangedType, + Reduce: p.reduceAPIConfigChanged, + }, + { + Event: project.APIConfigSecretChangedType, + Reduce: p.reduceAPIConfigSecretChanged, + }, + { + Event: project.OIDCConfigAddedType, + Reduce: p.reduceOIDCConfigAdded, + }, + { + Event: project.OIDCConfigChangedType, + Reduce: p.reduceOIDCConfigChanged, + }, + { + Event: project.OIDCConfigSecretChangedType, + Reduce: p.reduceOIDCConfigSecretChanged, + }, + }, + }, + } +} + +const ( + AppColumnID = "id" + AppColumnName = "name" + AppColumnProjectID = "project_id" + AppColumnCreationDate = "creation_date" + AppColumnChangeDate = "change_date" + AppColumnResourceOwner = "resource_owner" + AppColumnState = "state" + AppColumnSequence = "sequence" + + appAPITableSuffix = "api_configs" + AppAPIConfigColumnAppID = "app_id" + AppAPIConfigColumnClientID = "client_id" + AppAPIConfigColumnClientSecret = "client_secret" + AppAPIConfigColumnAuthMethod = "auth_method" + + appOIDCTableSuffix = "oidc_configs" + AppOIDCConfigColumnAppID = "app_id" + AppOIDCConfigColumnVersion = "version" + AppOIDCConfigColumnClientID = "client_id" + AppOIDCConfigColumnClientSecret = "client_secret" + AppOIDCConfigColumnRedirectUris = "redirect_uris" + AppOIDCConfigColumnResponseTypes = "response_types" + AppOIDCConfigColumnGrantTypes = "grant_types" + AppOIDCConfigColumnApplicationType = "application_type" + AppOIDCConfigColumnAuthMethodType = "auth_method_type" + AppOIDCConfigColumnPostLogoutRedirectUris = "post_logout_redirect_uris" + AppOIDCConfigColumnDevMode = "is_dev_mode" + AppOIDCConfigColumnAccessTokenType = "access_token_type" + AppOIDCConfigColumnAccessTokenRoleAssertion = "access_token_role_assertion" + AppOIDCConfigColumnIDTokenRoleAssertion = "id_token_role_assertion" + AppOIDCConfigColumnIDTokenUserinfoAssertion = "id_token_userinfo_assertion" + AppOIDCConfigColumnClockSkew = "clock_skew" + AppOIDCConfigColumnAdditionalOrigins = "additional_origins" +) + +func (p *AppProjection) reduceAppAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.ApplicationAddedEvent) + if !ok { + logging.LogWithFields("HANDL-OzK4m", "seq", event.Sequence(), "expectedType", project.ApplicationAddedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-1xYE6", "reduce.wrong.event.type") + } + return crdb.NewCreateStatement( + e, + []handler.Column{ + handler.NewCol(AppColumnID, e.AppID), + handler.NewCol(AppColumnName, e.Name), + handler.NewCol(AppColumnProjectID, e.Aggregate().ID), + handler.NewCol(AppColumnCreationDate, e.CreationDate()), + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(AppColumnState, domain.AppStateActive), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + ), nil +} + +func (p *AppProjection) reduceAppChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.ApplicationChangedEvent) + if !ok { + logging.LogWithFields("HANDL-4Fjh2", "seq", event.Sequence(), "expectedType", project.ApplicationChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-ZJ8JA", "reduce.wrong.event.type") + } + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(AppColumnName, e.Name), + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), nil +} + +func (p *AppProjection) reduceAppDeactivated(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.ApplicationDeactivatedEvent) + if !ok { + logging.LogWithFields("HANDL-hZ9to", "seq", event.Sequence(), "expectedType", project.ApplicationDeactivatedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-MVWxZ", "reduce.wrong.event.type") + } + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(AppColumnState, domain.AppStateInactive), + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), nil +} + +func (p *AppProjection) reduceAppReactivated(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.ApplicationReactivatedEvent) + if !ok { + logging.LogWithFields("HANDL-AbK3B", "seq", event.Sequence(), "expectedType", project.ApplicationReactivatedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-D0HZO", "reduce.wrong.event.type") + } + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(AppColumnState, domain.AppStateActive), + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), nil +} + +func (p *AppProjection) reduceAppRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.ApplicationRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-tdRId", "seq", event.Sequence(), "expectedType", project.ApplicationRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-Y99aq", "reduce.wrong.event.type") + } + return crdb.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), nil +} + +func (p *AppProjection) reduceAPIConfigAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.APIConfigAddedEvent) + if !ok { + logging.LogWithFields("HANDL-tdRId", "seq", event.Sequence(), "expectedType", project.APIConfigAddedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-Y99aq", "reduce.wrong.event.type") + } + return crdb.NewMultiStatement( + e, + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(AppAPIConfigColumnAppID, e.AppID), + handler.NewCol(AppAPIConfigColumnClientID, e.ClientID), + handler.NewCol(AppAPIConfigColumnClientSecret, e.ClientSecret), + handler.NewCol(AppAPIConfigColumnAuthMethod, e.AuthMethodType), + }, + crdb.WithTableSuffix(appAPITableSuffix), + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), + ), nil +} + +func (p *AppProjection) reduceAPIConfigChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.APIConfigChangedEvent) + if !ok { + logging.LogWithFields("HANDL-C6b4f", "seq", event.Sequence(), "expectedType", project.APIConfigChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-vnZKi", "reduce.wrong.event.type") + } + cols := make([]handler.Column, 0, 2) + if e.ClientSecret != nil { + cols = append(cols, handler.NewCol(AppAPIConfigColumnClientSecret, e.ClientSecret)) + } + if e.AuthMethodType != nil { + cols = append(cols, handler.NewCol(AppAPIConfigColumnAuthMethod, *e.AuthMethodType)) + } + if len(cols) == 0 { + return crdb.NewNoOpStatement(e), nil + } + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + cols, + []handler.Condition{ + handler.NewCond(AppAPIConfigColumnAppID, e.AppID), + }, + crdb.WithTableSuffix(appAPITableSuffix), + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), + ), nil +} + +func (p *AppProjection) reduceAPIConfigSecretChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.APIConfigSecretChangedEvent) + if !ok { + logging.LogWithFields("HANDL-dssSI", "seq", event.Sequence(), "expectedType", project.APIConfigSecretChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-ttb0I", "reduce.wrong.event.type") + } + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(AppAPIConfigColumnClientSecret, e.ClientSecret), + }, + []handler.Condition{ + handler.NewCond(AppAPIConfigColumnAppID, e.AppID), + }, + crdb.WithTableSuffix(appAPITableSuffix), + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), + ), nil +} + +func (p *AppProjection) reduceOIDCConfigAdded(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.OIDCConfigAddedEvent) + if !ok { + logging.LogWithFields("HANDL-nlDQv", "seq", event.Sequence(), "expectedType", project.OIDCConfigAddedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-GNHU1", "reduce.wrong.event.type") + } + return crdb.NewMultiStatement( + e, + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(AppOIDCConfigColumnAppID, e.AppID), + handler.NewCol(AppOIDCConfigColumnVersion, e.Version), + handler.NewCol(AppOIDCConfigColumnClientID, e.ClientID), + handler.NewCol(AppOIDCConfigColumnClientSecret, e.ClientSecret), + handler.NewCol(AppOIDCConfigColumnRedirectUris, pq.StringArray(e.RedirectUris)), + handler.NewCol(AppOIDCConfigColumnResponseTypes, pq.Array(e.ResponseTypes)), + handler.NewCol(AppOIDCConfigColumnGrantTypes, pq.Array(e.GrantTypes)), + handler.NewCol(AppOIDCConfigColumnApplicationType, e.ApplicationType), + handler.NewCol(AppOIDCConfigColumnAuthMethodType, e.AuthMethodType), + handler.NewCol(AppOIDCConfigColumnPostLogoutRedirectUris, pq.StringArray(e.PostLogoutRedirectUris)), + handler.NewCol(AppOIDCConfigColumnDevMode, e.DevMode), + handler.NewCol(AppOIDCConfigColumnAccessTokenType, e.AccessTokenType), + handler.NewCol(AppOIDCConfigColumnAccessTokenRoleAssertion, e.AccessTokenRoleAssertion), + handler.NewCol(AppOIDCConfigColumnIDTokenRoleAssertion, e.IDTokenRoleAssertion), + handler.NewCol(AppOIDCConfigColumnIDTokenUserinfoAssertion, e.IDTokenUserinfoAssertion), + handler.NewCol(AppOIDCConfigColumnClockSkew, e.ClockSkew), + handler.NewCol(AppOIDCConfigColumnAdditionalOrigins, pq.StringArray(e.AdditionalOrigins)), + }, + crdb.WithTableSuffix(appOIDCTableSuffix), + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), + ), nil +} + +func (p *AppProjection) reduceOIDCConfigChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.OIDCConfigChangedEvent) + if !ok { + logging.LogWithFields("HANDL-nlDQv", "seq", event.Sequence(), "expectedType", project.OIDCConfigChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-GNHU1", "reduce.wrong.event.type") + } + + cols := make([]handler.Column, 0, 15) + if e.Version != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnVersion, *e.Version)) + } + if e.RedirectUris != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnRedirectUris, pq.StringArray(*e.RedirectUris))) + } + if e.ResponseTypes != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnResponseTypes, pq.Array(*e.ResponseTypes))) + } + if e.GrantTypes != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnGrantTypes, pq.Array(*e.GrantTypes))) + } + if e.ApplicationType != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnApplicationType, *e.ApplicationType)) + } + if e.AuthMethodType != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnAuthMethodType, *e.AuthMethodType)) + } + if e.PostLogoutRedirectUris != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnPostLogoutRedirectUris, pq.StringArray(*e.PostLogoutRedirectUris))) + } + if e.DevMode != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnDevMode, *e.DevMode)) + } + if e.AccessTokenType != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnAccessTokenType, *e.AccessTokenType)) + } + if e.AccessTokenRoleAssertion != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnAccessTokenRoleAssertion, *e.AccessTokenRoleAssertion)) + } + if e.IDTokenRoleAssertion != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnIDTokenRoleAssertion, *e.IDTokenRoleAssertion)) + } + if e.IDTokenUserinfoAssertion != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnIDTokenUserinfoAssertion, *e.IDTokenUserinfoAssertion)) + } + if e.ClockSkew != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnClockSkew, *e.ClockSkew)) + } + if e.AdditionalOrigins != nil { + cols = append(cols, handler.NewCol(AppOIDCConfigColumnAdditionalOrigins, pq.StringArray(*e.AdditionalOrigins))) + } + + if len(cols) == 0 { + return crdb.NewNoOpStatement(e), nil + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + cols, + []handler.Condition{ + handler.NewCond(AppOIDCConfigColumnAppID, e.AppID), + }, + crdb.WithTableSuffix(appOIDCTableSuffix), + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), + ), nil +} + +func (p *AppProjection) reduceOIDCConfigSecretChanged(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*project.OIDCConfigSecretChangedEvent) + if !ok { + logging.LogWithFields("HANDL-nlDQv", "seq", event.Sequence(), "expectedType", project.OIDCConfigSecretChangedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-GNHU1", "reduce.wrong.event.type") + } + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(AppOIDCConfigColumnClientSecret, e.ClientSecret), + }, + []handler.Condition{ + handler.NewCond(AppOIDCConfigColumnAppID, e.AppID), + }, + crdb.WithTableSuffix(appOIDCTableSuffix), + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(AppColumnChangeDate, e.CreationDate()), + handler.NewCol(AppColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(AppColumnID, e.AppID), + }, + ), + ), nil +} diff --git a/internal/query/projection/app_test.go b/internal/query/projection/app_test.go new file mode 100644 index 0000000000..a0524ee40a --- /dev/null +++ b/internal/query/projection/app_test.go @@ -0,0 +1,544 @@ +package projection + +import ( + "testing" + "time" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/project" + "github.com/lib/pq" +) + +func TestAppProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.EventReader + } + tests := []struct { + name string + args args + reduce func(event eventstore.EventReader) (*handler.Statement, error) + want wantReduce + }{ + { + name: "project.reduceAppAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationAddedType), + project.AggregateType, + []byte(`{ + "appId": "app-id", + "name": "my-app" + }`), + ), project.ApplicationAddedEventMapper), + }, + reduce: (&AppProjection{}).reduceAppAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.apps (id, name, project_id, creation_date, change_date, resource_owner, state, sequence) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedArgs: []interface{}{ + "app-id", + "my-app", + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + domain.AppStateActive, + uint64(15), + }, + }, + }, + }, + }, + }, + { + name: "project.reduceAppChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationChangedType), + project.AggregateType, + []byte(`{ + "appId": "app-id", + "name": "my-app" + }`), + ), project.ApplicationChangedEventMapper), + }, + reduce: (&AppProjection{}).reduceAppChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.apps SET (name, change_date, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + "my-app", + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceAppDeactivated", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationDeactivatedType), + project.AggregateType, + []byte(`{ + "appId": "app-id" + }`), + ), project.ApplicationDeactivatedEventMapper), + }, + reduce: (&AppProjection{}).reduceAppDeactivated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.apps SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + domain.AppStateInactive, + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceAppReactivated", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationReactivatedType), + project.AggregateType, + []byte(`{ + "appId": "app-id" + }`), + ), project.ApplicationReactivatedEventMapper), + }, + reduce: (&AppProjection{}).reduceAppReactivated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.apps SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + domain.AppStateActive, + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceAppRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ApplicationRemovedType), + project.AggregateType, + []byte(`{ + "appId": "app-id" + }`), + ), project.ApplicationRemovedEventMapper), + }, + reduce: (&AppProjection{}).reduceAppRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.apps WHERE (id = $1)", + expectedArgs: []interface{}{ + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceAPIConfigAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.APIConfigAddedType), + project.AggregateType, + []byte(`{ + "appId": "app-id", + "clientId": "client-id", + "clientSecret": {}, + "authMethodType": 1 + }`), + ), project.APIConfigAddedEventMapper), + }, + reduce: (&AppProjection{}).reduceAPIConfigAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.apps_api_configs (app_id, client_id, client_secret, auth_method) VALUES ($1, $2, $3, $4)", + expectedArgs: []interface{}{ + "app-id", + "client-id", + anyArg{}, + domain.APIAuthMethodTypePrivateKeyJWT, + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.apps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceAPIConfigChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.APIConfigChangedType), + project.AggregateType, + []byte(`{ + "appId": "app-id", + "clientId": "client-id", + "clientSecret": {}, + "authMethodType": 1 + }`), + ), project.APIConfigChangedEventMapper), + }, + reduce: (&AppProjection{}).reduceAPIConfigChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.apps_api_configs SET (client_secret, auth_method) = ($1, $2) WHERE (app_id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + domain.APIAuthMethodTypePrivateKeyJWT, + "app-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.apps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceAPIConfigChanged noop", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.APIConfigChangedType), + project.AggregateType, + []byte(`{ + "appId": "app-id" + }`), + ), project.APIConfigChangedEventMapper), + }, + reduce: (&AppProjection{}).reduceAPIConfigChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{}, + }, + }, + }, + { + name: "project.reduceAPIConfigSecretChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.APIConfigSecretChangedType), + project.AggregateType, + []byte(`{ + "appId": "app-id", + "client_secret": {} + }`), + ), project.APIConfigSecretChangedEventMapper), + }, + reduce: (&AppProjection{}).reduceAPIConfigSecretChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.apps_api_configs SET (client_secret) = ($1) WHERE (app_id = $2)", + expectedArgs: []interface{}{ + anyArg{}, + "app-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.apps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceOIDCConfigAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.OIDCConfigAddedType), + project.AggregateType, + []byte(`{ + "oidcVersion": 0, + "appId": "app-id", + "clientId": "client-id", + "clientSecret": {}, + "redirectUris": ["redirect.one.ch", "redirect.two.ch"], + "responseTypes": [1,2], + "grantTypes": [1,2], + "applicationType": 2, + "authMethodType": 2, + "postLogoutRedirectUris": ["logout.one.ch", "logout.two.ch"], + "devMode": true, + "accessTokenType": 1, + "accessTokenRoleAssertion": true, + "idTokenRoleAssertion": true, + "idTokenUserinfoAssertion": true, + "clockSkew": 1000, + "additionalOrigins": ["origin.one.ch", "origin.two.ch"] + }`), + ), project.OIDCConfigAddedEventMapper), + }, + reduce: (&AppProjection{}).reduceOIDCConfigAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.apps_oidc_configs (app_id, version, client_id, client_secret, 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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)", + expectedArgs: []interface{}{ + "app-id", + domain.OIDCVersionV1, + "client-id", + anyArg{}, + pq.StringArray{"redirect.one.ch", "redirect.two.ch"}, + pq.Array([]domain.OIDCResponseType{1, 2}), + pq.Array([]domain.OIDCGrantType{1, 2}), + domain.OIDCApplicationTypeNative, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"logout.one.ch", "logout.two.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + true, + true, + 1 * time.Microsecond, + pq.StringArray{"origin.one.ch", "origin.two.ch"}, + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.apps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceOIDCConfigChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.OIDCConfigChangedType), + project.AggregateType, + []byte(`{ + "oidcVersion": 0, + "appId": "app-id", + "redirectUris": ["redirect.one.ch", "redirect.two.ch"], + "responseTypes": [1,2], + "grantTypes": [1,2], + "applicationType": 2, + "authMethodType": 2, + "postLogoutRedirectUris": ["logout.one.ch", "logout.two.ch"], + "devMode": true, + "accessTokenType": 1, + "accessTokenRoleAssertion": true, + "idTokenRoleAssertion": true, + "idTokenUserinfoAssertion": true, + "clockSkew": 1000, + "additionalOrigins": ["origin.one.ch", "origin.two.ch"] + }`), + ), project.OIDCConfigChangedEventMapper), + }, + reduce: (&AppProjection{}).reduceOIDCConfigChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.apps_oidc_configs SET (version, 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) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) WHERE (app_id = $15)", + expectedArgs: []interface{}{ + domain.OIDCVersionV1, + pq.StringArray{"redirect.one.ch", "redirect.two.ch"}, + pq.Array([]domain.OIDCResponseType{1, 2}), + pq.Array([]domain.OIDCGrantType{1, 2}), + domain.OIDCApplicationTypeNative, + domain.OIDCAuthMethodTypeNone, + pq.StringArray{"logout.one.ch", "logout.two.ch"}, + true, + domain.OIDCTokenTypeJWT, + true, + true, + true, + 1 * time.Microsecond, + pq.StringArray{"origin.one.ch", "origin.two.ch"}, + "app-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.apps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + { + name: "project.reduceOIDCConfigChanged noop", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.OIDCConfigChangedType), + project.AggregateType, + []byte(`{ + "appId": "app-id" + }`), + ), project.OIDCConfigChangedEventMapper), + }, + reduce: (&AppProjection{}).reduceOIDCConfigChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{}, + }, + }, + }, + { + name: "project.reduceOIDCConfigSecretChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.OIDCConfigSecretChangedType), + project.AggregateType, + []byte(`{ + "appId": "app-id", + "client_secret": {} + }`), + ), project.OIDCConfigSecretChangedEventMapper), + }, + reduce: (&AppProjection{}).reduceOIDCConfigSecretChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("project"), + sequence: 15, + previousSequence: 10, + projection: AppProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.apps_oidc_configs SET (client_secret) = ($1) WHERE (app_id = $2)", + expectedArgs: []interface{}{ + anyArg{}, + "app-id", + }, + }, + { + expectedStmt: "UPDATE zitadel.projections.apps SET (change_date, sequence) = ($1, $2) WHERE (id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "app-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if _, ok := err.(errors.InvalidArgument); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, tt.want) + }) + } +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index b27a5be24a..b121aefbdb 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -48,6 +48,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co NewOrgDomainProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["org_domains"])) NewLoginPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["login_policies"])) NewIDPProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idps"])) + NewAppProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["apps"])) NewMailTemplateProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["mail_templates"])) NewMessageTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["message_texts"])) NewCustomTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["custom_texts"])) diff --git a/migrations/cockroach/V1.87__app.sql b/migrations/cockroach/V1.87__app.sql new file mode 100644 index 0000000000..f798e963ca --- /dev/null +++ b/migrations/cockroach/V1.87__app.sql @@ -0,0 +1,47 @@ +CREATE TABLE zitadel.projections.apps( + id STRING, + project_id STRING NOT NULL, + creation_date TIMESTAMPTZ, + change_date TIMESTAMPTZ, + resource_owner STRING NOT NULL, + state INT2, + sequence INT8, + + name STRING, + + PRIMARY KEY (id), + INDEX idx_project_id (project_id) +); + +CREATE TABLE zitadel.projections.apps_api_configs( + app_id STRING REFERENCES zitadel.projections.apps (id) ON DELETE CASCADE, + + client_id STRING NOT NULL, + client_secret JSONB, + auth_method INT2, + + PRIMARY KEY (app_id) +); + +CREATE TABLE zitadel.projections.apps_oidc_configs( + app_id STRING REFERENCES zitadel.projections.apps (id) ON DELETE CASCADE, + + version STRING NOT NULL, + client_id STRING NOT NULL, + client_secret JSONB, + redirect_uris STRING[], + response_types INT2[], + grant_types INT2[], + application_type INT2, + auth_method_type INT2, + post_logout_redirect_uris STRING[], + is_dev_mode BOOLEAN, + access_token_type INT2, + access_token_role_assertion BOOLEAN, + id_token_role_assertion BOOLEAN, + id_token_userinfo_assertion BOOLEAN, + clock_skew INT8, + additional_origins STRING[], + + PRIMARY KEY (app_id) +);