diff --git a/internal/domain/idp.go b/internal/domain/idp.go index bea106298..c0fac9fd9 100644 --- a/internal/domain/idp.go +++ b/internal/domain/idp.go @@ -38,6 +38,7 @@ const ( IDPTypeGoogle IDPTypeApple IDPTypeSAML + IDPTypeZitadel ) func (t IDPType) GetCSSClass() string { diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index f6fb7db30..c6bee0ff6 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -30,6 +30,7 @@ const ( IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix IDPTemplateAppleTable = IDPTemplateTable + "_" + IDPTemplateAppleSuffix IDPTemplateSAMLTable = IDPTemplateTable + "_" + IDPTemplateSAMLSuffix + IDPTemplateZitadelTable = IDPTemplateTable + "_" + IDPTemplateZitadelSuffix IDPTemplateOAuthSuffix = "oauth2" IDPTemplateOIDCSuffix = "oidc" @@ -43,6 +44,7 @@ const ( IDPTemplateLDAPSuffix = "ldap2" IDPTemplateAppleSuffix = "apple" IDPTemplateSAMLSuffix = "saml" + IDPTemplateZitadelSuffix = "zitadel" IDPTemplateIDCol = "id" IDPTemplateCreationDateCol = "creation_date" @@ -175,6 +177,14 @@ const ( SAMLNameIDFormatCol = "name_id_format" SAMLTransientMappingAttributeName = "transient_mapping_attribute_name" SAMLFederatedLogoutEnabled = "federated_logout_enabled" + + ZitadelIDCol = "idp_id" + ZitadelInstanceIDCol = "instance_id" + ZitadelIssuerCol = "issuer" + ZitadelClientIDCol = "client_id" + ZitadelClientSecretCol = "client_secret" + ZitadelScopesCol = "scopes" + ZitadelInstanceRolesInfoCol = "instance_roles_info" ) type idpTemplateProjection struct{} @@ -386,6 +396,19 @@ func (*idpTemplateProjection) Init() *old_handler.Check { IDPTemplateSAMLSuffix, handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), ), + handler.NewSuffixedTable([]*handler.InitColumn{ + handler.NewColumn(ZitadelIDCol, handler.ColumnTypeText), + handler.NewColumn(ZitadelInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(ZitadelIssuerCol, handler.ColumnTypeText), + handler.NewColumn(ZitadelClientIDCol, handler.ColumnTypeText), + handler.NewColumn(ZitadelClientSecretCol, handler.ColumnTypeJSONB), + handler.NewColumn(ZitadelScopesCol, handler.ColumnTypeTextArray, handler.Nullable()), + handler.NewColumn(ZitadelInstanceRolesInfoCol, handler.ColumnTypeJSONB, handler.Nullable()), + }, + handler.NewPrimaryKey(ZitadelInstanceIDCol, ZitadelIDCol), + IDPTemplateZitadelSuffix, + handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), + ), ) } @@ -522,6 +545,10 @@ func (p *idpTemplateProjection) Reducers() []handler.AggregateReducer { Event: instance.SAMLIDPChangedEventType, Reduce: p.reduceSAMLIDPChanged, }, + { + Event: instance.ZitadelIDPAddedEventType, + Reduce: p.reduceZitadelIDPAdded, + }, { Event: instance.IDPConfigRemovedEventType, Reduce: p.reduceIDPConfigRemoved, @@ -2539,3 +2566,55 @@ func reduceSAMLIDPChangedColumns(idpEvent idp.SAMLIDPChangedEvent) []handler.Col } return SAMLCols } + +func (p *idpTemplateProjection) reduceZitadelIDPAdded(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.ZitadelIDPAddedEvent + var idpOwnerType domain.IdentityProviderType + switch e := event.(type) { + case *instance.ZitadelIDPAddedEvent: + idpEvent = e.ZitadelIDPAddedEvent + idpOwnerType = domain.IdentityProviderTypeSystem + // todo (@grvijayan): org-level ZitadelIDPAddedEvent in the next PRs + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ZCpH41", "reduce.wrong.event.type %v", []eventstore.EventType{instance.ZitadelIDPAddedEventType}) + } + + zitadelIDPCols := []handler.Column{ + handler.NewCol(ZitadelIDCol, idpEvent.ID), + handler.NewCol(ZitadelInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(ZitadelIssuerCol, idpEvent.Issuer), + handler.NewCol(ZitadelClientIDCol, idpEvent.ClientID), + handler.NewCol(ZitadelClientSecretCol, idpEvent.ClientSecret), + handler.NewCol(ZitadelScopesCol, database.TextArray[string](idpEvent.Scopes)), + } + if len(idpEvent.InstanceRolesInfo) > 0 { + zitadelIDPCols = append(zitadelIDPCols, handler.NewJSONCol(ZitadelInstanceRolesInfoCol, idpEvent.InstanceRolesInfo)) + } + + return handler.NewMultiStatement( + &idpEvent, + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(IDPTemplateIDCol, idpEvent.ID), + handler.NewCol(IDPTemplateCreationDateCol, idpEvent.CreationDate()), + handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()), + handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTemplateResourceOwnerCol, idpEvent.Aggregate().ResourceOwner), + handler.NewCol(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(IDPTemplateStateCol, domain.IDPStateActive), + handler.NewCol(IDPTemplateNameCol, idpEvent.Name), + handler.NewCol(IDPTemplateOwnerTypeCol, idpOwnerType), + handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeZitadel), + handler.NewCol(IDPTemplateIsCreationAllowedCol, idpEvent.IsCreationAllowed), + handler.NewCol(IDPTemplateIsLinkingAllowedCol, idpEvent.IsLinkingAllowed), + handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.IsAutoCreation), + handler.NewCol(IDPTemplateIsAutoUpdateCol, idpEvent.IsAutoUpdate), + handler.NewCol(IDPTemplateAutoLinkingCol, idpEvent.AutoLinkingOption), + }, + ), + handler.AddCreateStatement( + zitadelIDPCols, + handler.WithTableSuffix(IDPTemplateZitadelSuffix), + ), + ), nil +} diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index d127e9b84..2c20e4792 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -4505,3 +4505,172 @@ func stringToJSONByte(data string) string { jsondata, _ := json.Marshal([]byte(data)) return string(jsondata) } + +func TestIDPTemplateProjection_reducesZitadel(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "instance reduceZitadelIDPAdded without instance roles info", + args: args{ + event: getEvent( + testEvent( + instance.ZitadelIDPAddedEventType, + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "idp-name", + "issuer": "issuer", + "clientId": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true, + "autoLinkingOption": 1 +}`), + ), eventstore.GenericEventMapper[instance.ZitadelIDPAddedEvent]), + }, + reduce: (&idpTemplateProjection{}).reduceZitadelIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateInsertStmt, + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "idp-name", + domain.IdentityProviderTypeSystem, + domain.IDPTypeZitadel, + true, + true, + true, + true, + domain.AutoLinkingOptionUsername, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates6_zitadel (idp_id, instance_id, issuer, client_id, client_secret, scopes) VALUES ($1, $2, $3, $4, $5, $6)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "issuer", + "client_id", + anyArg{}, + database.TextArray[string]{"profile"}, + }, + }, + }, + }, + }, + }, + { + name: "instance reduceZitadelIDPAdded with instance roles info", + args: args{ + event: getEvent( + testEvent( + instance.ZitadelIDPAddedEventType, + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "idp-name", + "issuer": "issuer", + "clientId": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true, + "autoLinkingOption": 1, + "instanceRolesInfo": [{ + "organizationId": "org1", + "organizationDomain": "org1.com" + }, + { + "organizationId": "org2", + "organizationDomain": "org2.com" + }] +}`), + ), eventstore.GenericEventMapper[instance.ZitadelIDPAddedEvent]), + }, + reduce: (&idpTemplateProjection{}).reduceZitadelIDPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: idpTemplateInsertStmt, + expectedArgs: []interface{}{ + "idp-id", + anyArg{}, + anyArg{}, + uint64(15), + "ro-id", + "instance-id", + domain.IDPStateActive, + "idp-name", + domain.IdentityProviderTypeSystem, + domain.IDPTypeZitadel, + true, + true, + true, + true, + domain.AutoLinkingOptionUsername, + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates6_zitadel (idp_id, instance_id, issuer, client_id, client_secret, scopes, instance_roles_info) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "issuer", + "client_id", + anyArg{}, + database.TextArray[string]{"profile"}, + []byte(`[{"organizationId":"org1","organizationDomain":"org1.com"},{"organizationId":"org2","organizationDomain":"org2.com"}]`), + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if !zerrors.IsErrorInvalidArgument(err) { + 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, IDPTemplateTable, tt.want) + }) + } +}