diff --git a/internal/query/features.go b/internal/query/features.go new file mode 100644 index 0000000000..4208407e5b --- /dev/null +++ b/internal/query/features.go @@ -0,0 +1,230 @@ +package query + +import ( + "context" + "database/sql" + errs "errors" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query/projection" +) + +type Feature struct { + AggregateID string + CreationDate time.Time + ChangeDate time.Time + Sequence uint64 + IsDefault bool + TierName string + TierDescription string + State domain.FeaturesState + StateDescription string + AuditLogRetention time.Duration + LoginPolicyFactors bool + LoginPolicyIDP bool + LoginPolicyPasswordless bool + LoginPolicyRegistration bool + LoginPolicyUsernameLogin bool + LoginPolicyPasswordReset bool + PasswordComplexityPolicy bool + LabelPolicyPrivateLabel bool + LabelPolicyWatermark bool + CustomDomain bool + PrivacyPolicy bool + MetadataUser bool + CustomTextMessage bool + CustomTextLogin bool + LockoutPolicy bool + Actions bool +} + +var ( + feautureTable = table{ + name: projection.FeatureTable, + } + FeatureColumnAggregateID = Column{ + name: projection.FeatureAggregateIDCol, + } + FeatureColumnCreationDate = Column{ + name: projection.FeatureCreationDateCol, + } + FeatureColumnChangeDate = Column{ + name: projection.FeatureChangeDateCol, + } + FeatureColumnSequence = Column{ + name: projection.FeatureSequenceCol, + } + FeatureColumnIsDefault = Column{ + name: projection.FeatureIsDefaultCol, + } + FeatureTierName = Column{ + name: projection.FeatureTierNameCol, + } + FeatureTierDescription = Column{ + name: projection.FeatureTierDescriptionCol, + } + FeatureState = Column{ + name: projection.FeatureStateCol, + } + FeatureStateDescription = Column{ + name: projection.FeatureStateDescriptionCol, + } + FeatureAuditLogRetention = Column{ + name: projection.FeatureAuditLogRetentionCol, + } + FeatureLoginPolicyFactors = Column{ + name: projection.FeatureLoginPolicyFactorsCol, + } + FeatureLoginPolicyIDP = Column{ + name: projection.FeatureLoginPolicyIDPCol, + } + FeatureLoginPolicyPasswordless = Column{ + name: projection.FeatureLoginPolicyPasswordlessCol, + } + FeatureLoginPolicyRegistration = Column{ + name: projection.FeatureLoginPolicyRegistrationCol, + } + FeatureLoginPolicyUsernameLogin = Column{ + name: projection.FeatureLoginPolicyUsernameLoginCol, + } + FeatureLoginPolicyPasswordReset = Column{ + name: projection.FeatureLoginPolicyPasswordResetCol, + } + FeaturePasswordComplexityPolicy = Column{ + name: projection.FeaturePasswordComplexityPolicyCol, + } + FeatureLabelPolicyPrivateLabel = Column{ + name: projection.FeatureLabelPolicyPrivateLabelCol, + } + FeatureLabelPolicyWatermark = Column{ + name: projection.FeatureLabelPolicyWatermarkCol, + } + FeatureCustomDomain = Column{ + name: projection.FeatureCustomDomainCol, + } + FeaturePrivacyPolicy = Column{ + name: projection.FeaturePrivacyPolicyCol, + } + FeatureMetadataUser = Column{ + name: projection.FeatureMetadataUserCol, + } + FeatureCustomTextMessage = Column{ + name: projection.FeatureCustomTextMessageCol, + } + FeatureCustomTextLogin = Column{ + name: projection.FeatureCustomTextLoginCol, + } + FeatureLockoutPolicy = Column{ + name: projection.FeatureLockoutPolicyCol, + } + FeatureActions = Column{ + name: projection.FeatureActionsCol, + } +) + +func (q *Queries) FeatureByID(ctx context.Context, orgID string) (*Feature, error) { + query, scan := prepareFeatureQuery() + stmt, args, err := query.Where( + sq.Or{ + sq.Eq{ + FeatureColumnAggregateID.identifier(): orgID, + }, + sq.Eq{ + FeatureColumnAggregateID.identifier(): domain.IAMID, + }, + }). + OrderBy(FeatureColumnIsDefault.identifier()). + Limit(1).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-P9gwg", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + +func (q *Queries) DefaultFeature(ctx context.Context) (*Feature, error) { + query, scan := prepareFeatureQuery() + stmt, args, err := query.Where(sq.Eq{ + FeatureColumnAggregateID.identifier(): domain.IAMID, + }).OrderBy(FeatureColumnIsDefault.identifier()).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-1Ndlg", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + +func prepareFeatureQuery() (sq.SelectBuilder, func(*sql.Row) (*Feature, error)) { + return sq.Select( + FeatureColumnAggregateID.identifier(), + FeatureColumnCreationDate.identifier(), + FeatureColumnChangeDate.identifier(), + FeatureColumnSequence.identifier(), + FeatureColumnIsDefault.identifier(), + FeatureTierName.identifier(), + FeatureTierDescription.identifier(), + FeatureState.identifier(), + FeatureStateDescription.identifier(), + FeatureAuditLogRetention.identifier(), + FeatureLoginPolicyFactors.identifier(), + FeatureLoginPolicyIDP.identifier(), + FeatureLoginPolicyPasswordless.identifier(), + FeatureLoginPolicyRegistration.identifier(), + FeatureLoginPolicyUsernameLogin.identifier(), + FeatureLoginPolicyPasswordReset.identifier(), + FeaturePasswordComplexityPolicy.identifier(), + FeatureLabelPolicyPrivateLabel.identifier(), + FeatureLabelPolicyWatermark.identifier(), + FeatureCustomDomain.identifier(), + FeaturePrivacyPolicy.identifier(), + FeatureMetadataUser.identifier(), + FeatureCustomTextMessage.identifier(), + FeatureCustomTextLogin.identifier(), + FeatureLockoutPolicy.identifier(), + FeatureActions.identifier(), + ).From(loginPolicyTable.identifier()).PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Feature, error) { + p := new(Feature) + err := row.Scan( + &p.AggregateID, + &p.CreationDate, + &p.ChangeDate, + &p.Sequence, + &p.IsDefault, + &p.TierName, + &p.TierDescription, + &p.State, + &p.StateDescription, + &p.AuditLogRetention, + &p.LoginPolicyFactors, + &p.LoginPolicyIDP, + &p.LoginPolicyPasswordless, + &p.LoginPolicyRegistration, + &p.LoginPolicyRegistration, + &p.LoginPolicyUsernameLogin, + &p.LoginPolicyPasswordReset, + &p.PasswordComplexityPolicy, + &p.LabelPolicyPrivateLabel, + &p.LabelPolicyWatermark, + &p.CustomDomain, + &p.PrivacyPolicy, + &p.MetadataUser, + &p.CustomTextMessage, + &p.CustomTextLogin, + &p.LockoutPolicy, + &p.Actions, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-M9fse", "Errors.Feature.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-3o9gd", "Errors.Internal") + } + return p, nil + } +} diff --git a/internal/query/projection/feature.go b/internal/query/projection/feature.go new file mode 100644 index 0000000000..63f93d0ef4 --- /dev/null +++ b/internal/query/projection/feature.go @@ -0,0 +1,191 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/repository/features" + + "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/iam" + "github.com/caos/zitadel/internal/repository/org" +) + +type FeatureProjection struct { + crdb.StatementHandler +} + +const ( + FeatureTable = "zitadel.projections.features" +) + +func NewFeatureProjection(ctx context.Context, config crdb.StatementHandlerConfig) *FeatureProjection { + p := &FeatureProjection{} + config.ProjectionName = FeatureTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *FeatureProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: org.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: org.FeaturesSetEventType, + Reduce: p.reduceFeatureSet, + }, + { + Event: org.FeaturesRemovedEventType, + Reduce: p.reduceFeatureRemoved, + }, + }, + }, + { + Aggregate: iam.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: iam.FeaturesSetEventType, + Reduce: p.reduceFeatureSet, + }, + }, + }, + } +} + +const ( + FeatureAggregateIDCol = "aggregate_id" + FeatureCreationDateCol = "creation_date" + FeatureChangeDateCol = "change_date" + FeatureSequenceCol = "sequence" + FeatureIsDefaultCol = "is_default" + FeatureTierNameCol = "tier_name" + FeatureTierDescriptionCol = "tier_description" + FeatureStateCol = "state" + FeatureStateDescriptionCol = "state_description" + FeatureAuditLogRetentionCol = "audit_log_retention" + FeatureLoginPolicyFactorsCol = "login_policy_factors" + FeatureLoginPolicyIDPCol = "login_policy_idp" + FeatureLoginPolicyPasswordlessCol = "login_policy_passwordless" + FeatureLoginPolicyRegistrationCol = "login_policy_registration" + FeatureLoginPolicyUsernameLoginCol = "login_policy_username_login" + FeatureLoginPolicyPasswordResetCol = "login_policy_password_reset" + FeaturePasswordComplexityPolicyCol = "password_complexity_policy" + FeatureLabelPolicyPrivateLabelCol = "label_policy_private_label" + FeatureLabelPolicyWatermarkCol = "label_policy_watermark" + FeatureCustomDomainCol = "custom_domain" + FeaturePrivacyPolicyCol = "privacy_policy" + FeatureMetadataUserCol = "metadata_user" + FeatureCustomTextMessageCol = "custom_text_message" + FeatureCustomTextLoginCol = "custom_text_login" + FeatureLockoutPolicyCol = "lockout_policy" + FeatureActionsCol = "actions" +) + +func (p *FeatureProjection) reduceFeatureSet(event eventstore.EventReader) (*handler.Statement, error) { + var featureEvent features.FeaturesSetEvent + var isDefault bool + switch e := event.(type) { + case *iam.FeaturesSetEvent: + featureEvent = e.FeaturesSetEvent + isDefault = true + case *org.FeaturesSetEvent: + featureEvent = e.FeaturesSetEvent + isDefault = false + default: + logging.LogWithFields("HANDL-M9ets", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{org.FeaturesSetEventType, iam.FeaturesSetEventType}).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-K0erf", "reduce.wrong.event.type") + } + + cols := []handler.Column{ + handler.NewCol(FeatureAggregateIDCol, featureEvent.Aggregate().ID), + handler.NewCol(FeatureCreationDateCol, featureEvent.CreationDate()), + handler.NewCol(FeatureChangeDateCol, featureEvent.CreationDate()), + handler.NewCol(FeatureSequenceCol, featureEvent.Sequence()), + handler.NewCol(FeatureIsDefaultCol, isDefault), + } + if featureEvent.TierName != nil { + cols = append(cols, handler.NewCol(FeatureTierNameCol, *featureEvent.TierName)) + } + if featureEvent.TierDescription != nil { + cols = append(cols, handler.NewCol(FeatureTierDescriptionCol, *featureEvent.TierDescription)) + } + if featureEvent.State != nil { + cols = append(cols, handler.NewCol(FeatureStateCol, *featureEvent.State)) + } + if featureEvent.StateDescription != nil { + cols = append(cols, handler.NewCol(FeatureStateDescriptionCol, *featureEvent.StateDescription)) + } + if featureEvent.AuditLogRetention != nil { + cols = append(cols, handler.NewCol(FeatureAuditLogRetentionCol, *featureEvent.AuditLogRetention)) + } + if featureEvent.LoginPolicyFactors != nil { + cols = append(cols, handler.NewCol(FeatureLoginPolicyFactorsCol, *featureEvent.LoginPolicyFactors)) + } + if featureEvent.LoginPolicyIDP != nil { + cols = append(cols, handler.NewCol(FeatureLoginPolicyIDPCol, *featureEvent.LoginPolicyIDP)) + } + if featureEvent.LoginPolicyPasswordless != nil { + cols = append(cols, handler.NewCol(FeatureLoginPolicyPasswordlessCol, *featureEvent.LoginPolicyPasswordless)) + } + if featureEvent.LoginPolicyRegistration != nil { + cols = append(cols, handler.NewCol(FeatureLoginPolicyRegistrationCol, *featureEvent.LoginPolicyRegistration)) + } + if featureEvent.LoginPolicyUsernameLogin != nil { + cols = append(cols, handler.NewCol(FeatureLoginPolicyUsernameLoginCol, *featureEvent.LoginPolicyUsernameLogin)) + } + if featureEvent.LoginPolicyPasswordReset != nil { + cols = append(cols, handler.NewCol(FeatureLoginPolicyPasswordResetCol, *featureEvent.LoginPolicyPasswordReset)) + } + if featureEvent.PasswordComplexityPolicy != nil { + cols = append(cols, handler.NewCol(FeaturePasswordComplexityPolicyCol, *featureEvent.PasswordComplexityPolicy)) + } + if featureEvent.LabelPolicyPrivateLabel != nil { + cols = append(cols, handler.NewCol(FeatureLabelPolicyPrivateLabelCol, *featureEvent.LabelPolicyPrivateLabel)) + } + if featureEvent.LabelPolicyWatermark != nil { + cols = append(cols, handler.NewCol(FeatureLabelPolicyWatermarkCol, *featureEvent.LabelPolicyWatermark)) + } + if featureEvent.CustomDomain != nil { + cols = append(cols, handler.NewCol(FeatureCustomDomainCol, *featureEvent.CustomDomain)) + } + if featureEvent.PrivacyPolicy != nil { + cols = append(cols, handler.NewCol(FeaturePrivacyPolicyCol, *featureEvent.PrivacyPolicy)) + } + if featureEvent.MetadataUser != nil { + cols = append(cols, handler.NewCol(FeatureMetadataUserCol, *featureEvent.MetadataUser)) + } + if featureEvent.CustomTextMessage != nil { + cols = append(cols, handler.NewCol(FeatureCustomTextMessageCol, *featureEvent.CustomTextMessage)) + } + if featureEvent.CustomTextLogin != nil { + cols = append(cols, handler.NewCol(FeatureCustomTextLoginCol, *featureEvent.CustomTextLogin)) + } + if featureEvent.LockoutPolicy != nil { + cols = append(cols, handler.NewCol(FeatureLockoutPolicyCol, *featureEvent.LockoutPolicy)) + } + if featureEvent.Actions != nil { + cols = append(cols, handler.NewCol(FeatureActionsCol, *featureEvent.Actions)) + } + return crdb.NewUpsertStatement( + &featureEvent, + cols), nil +} + +func (p *FeatureProjection) reduceFeatureRemoved(event eventstore.EventReader) (*handler.Statement, error) { + e, ok := event.(*org.FeaturesRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-fN903", "seq", event.Sequence(), "expectedType", org.FeaturesRemovedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-0p4rf", "reduce.wrong.event.type") + } + return crdb.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(FeatureAggregateIDCol, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/feature_test.go b/internal/query/projection/feature_test.go new file mode 100644 index 0000000000..ed71f8f306 --- /dev/null +++ b/internal/query/projection/feature_test.go @@ -0,0 +1,215 @@ +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/iam" + "github.com/caos/zitadel/internal/repository/org" +) + +func TestFeatureProjection_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: "org.reduceFeatureSet", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.FeaturesSetEventType), + org.AggregateType, + []byte(`{ + "tierName": "TierName", + "tierDescription": "TierDescription", + "state": 1, + "stateDescription": "StateDescription", + "auditLogRetention": 1, + "loginPolicyFactors": true, + "loginPolicyIDP": true, + "loginPolicyPasswordless": true, + "loginPolicyRegistration": true, + "loginPolicyUsernameLogin": true, + "loginPolicyPasswordReset": true, + "passwordComplexityPolicy": true, + "labelPolicyPrivateLabel": true, + "labelPolicyWatermark": true, + "customDomain": true, + "privacyPolicy": true, + "metadataUser": true, + "customTextMessage": true, + "customTextLogin": true, + "lockoutPolicy": true, + "actions": true + }`), + ), org.FeaturesSetEventMapper), + }, + reduce: (&FeatureProjection{}).reduceFeatureSet, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + projection: FeatureTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, creation_date, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + uint64(15), + false, + "TierName", + "TierDescription", + domain.FeaturesStateActive, + "StateDescription", + time.Nanosecond, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + }, + }, + }, + }, + }, + }, + { + name: "org.reduceFeatureRemoved", + reduce: (&FeatureProjection{}).reduceFeatureRemoved, + args: args{ + event: getEvent(testEvent( + repository.EventType(org.FeaturesRemovedEventType), + org.AggregateType, + nil, + ), org.FeaturesRemovedEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + projection: FeatureTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.features WHERE (aggregate_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "iam.reduceFeatureSet", + reduce: (&FeatureProjection{}).reduceFeatureSet, + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.FeaturesSetEventType), + iam.AggregateType, + []byte(`{ + "tierName": "TierName", + "tierDescription": "TierDescription", + "state": 1, + "stateDescription": "StateDescription", + "auditLogRetention": 1, + "loginPolicyFactors": true, + "loginPolicyIDP": true, + "loginPolicyPasswordless": true, + "loginPolicyRegistration": true, + "loginPolicyUsernameLogin": true, + "loginPolicyPasswordReset": true, + "passwordComplexityPolicy": true, + "labelPolicyPrivateLabel": true, + "labelPolicyWatermark": true, + "customDomain": true, + "privacyPolicy": true, + "metadataUser": true, + "customTextMessage": true, + "customTextLogin": true, + "lockoutPolicy": true, + "actions": true + }`), + ), iam.FeaturesSetEventMapper), + }, + want: wantReduce{ + aggregateType: eventstore.AggregateType("iam"), + sequence: 15, + previousSequence: 10, + projection: FeatureTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, creation_date, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + uint64(15), + true, + "TierName", + "TierDescription", + domain.FeaturesStateActive, + "StateDescription", + time.Nanosecond, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + }, + }, + }, + }, + }, + }, + } + 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 26b015908a..b27a5be24a 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -51,6 +51,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co NewMailTemplateProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["mail_templates"])) NewMessageTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["message_texts"])) NewCustomTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["custom_texts"])) + NewFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["features"])) return nil } diff --git a/migrations/cockroach/V1.86__features.sql b/migrations/cockroach/V1.86__features.sql new file mode 100644 index 0000000000..9b890dff6e --- /dev/null +++ b/migrations/cockroach/V1.86__features.sql @@ -0,0 +1,32 @@ +CREATE TABLE zitadel.projections.features +( + aggregate_id TEXT NOT NULL, + creation_date TIMESTAMPTZ NULL, + change_date TIMESTAMPTZ NULL, + sequence INT8 NULL, + is_default BOOLEAN, + + tier_name TEXT, + tier_description TEXT, + state INT2 NULL, + state_description TEXT, + audit_log_retention BIGINT, + login_policy_factors BOOLEAN, + login_policy_idp BOOLEAN, + login_policy_passwordless BOOLEAN, + login_policy_registration BOOLEAN, + login_policy_username_login BOOLEAN, + login_policy_password_reset BOOLEAN, + password_complexity_policy BOOLEAN, + label_policy_private_label BOOLEAN, + label_policy_watermark BOOLEAN, + custom_domain BOOLEAN, + privacy_policy BOOLEAN, + metadata_user BOOLEAN, + custom_text_message BOOLEAN, + custom_text_login BOOLEAN, + lockout_policy BOOLEAN, + actions BOOLEAN, + + PRIMARY KEY (aggregate_id) +); \ No newline at end of file