package projection import ( "context" "fmt" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler" "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/policy" "github.com/zitadel/zitadel/internal/repository/user" ) const ( LoginNameProjectionTable = "projections.login_names" LoginNameUserProjectionTable = LoginNameProjectionTable + "_" + loginNameUserSuffix LoginNamePolicyProjectionTable = LoginNameProjectionTable + "_" + loginNamePolicySuffix LoginNameDomainProjectionTable = LoginNameProjectionTable + "_" + loginNameDomainSuffix LoginNameCol = "login_name" loginNameUserSuffix = "users" LoginNameUserIDCol = "id" LoginNameUserUserNameCol = "user_name" LoginNameUserResourceOwnerCol = "resource_owner" LoginNameUserInstanceIDCol = "instance_id" loginNameDomainSuffix = "domains" LoginNameDomainNameCol = "name" LoginNameDomainIsPrimaryCol = "is_primary" LoginNameDomainResourceOwnerCol = "resource_owner" LoginNameDomainInstanceIDCol = "instance_id" loginNamePolicySuffix = "policies" LoginNamePoliciesMustBeDomainCol = "must_be_domain" LoginNamePoliciesIsDefaultCol = "is_default" LoginNamePoliciesResourceOwnerCol = "resource_owner" LoginNamePoliciesInstanceIDCol = "instance_id" ) var ( viewStmt = fmt.Sprintf("SELECT"+ " user_id"+ " , (CASE WHEN %[1]s THEN CONCAT(%[2]s, '@', domain) ELSE %[2]s END) AS login_name"+ " , COALESCE(%[3]s, true) AS %[3]s"+ " , %[4]s"+ " FROM ("+ " SELECT"+ " policy_users.user_id"+ " , policy_users.%[2]s"+ " , policy_users.%[5]s"+ " , policy_users.%[4]s"+ " , policy_users.%[1]s"+ " , domains.%[6]s AS domain"+ " , domains.%[3]s"+ " FROM ("+ " SELECT"+ " users.id as user_id"+ " , users.%[2]s"+ " , users.%[4]s"+ " , users.%[5]s"+ " , COALESCE(policy_custom.%[1]s, policy_default.%[1]s) AS %[1]s"+ " FROM %[7]s users"+ " LEFT JOIN %[8]s policy_custom on policy_custom.%[9]s = users.%[5]s AND policy_custom.%[10]s = users.%[4]s"+ " LEFT JOIN %[8]s policy_default on policy_default.%[11]s = true AND policy_default.%[10]s = users.%[4]s) policy_users"+ " LEFT JOIN %[12]s domains ON policy_users.%[1]s AND policy_users.%[5]s = domains.%[13]s AND policy_users.%[10]s = domains.%[14]s"+ ") AS login_names;", LoginNamePoliciesMustBeDomainCol, LoginNameUserUserNameCol, LoginNameDomainIsPrimaryCol, LoginNameUserInstanceIDCol, LoginNameUserResourceOwnerCol, LoginNameDomainNameCol, LoginNameUserProjectionTable, LoginNamePolicyProjectionTable, LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesInstanceIDCol, LoginNamePoliciesIsDefaultCol, LoginNameDomainProjectionTable, LoginNameDomainResourceOwnerCol, LoginNameDomainInstanceIDCol, ) ) type loginNameProjection struct { crdb.StatementHandler } func newLoginNameProjection(ctx context.Context, config crdb.StatementHandlerConfig) *loginNameProjection { p := new(loginNameProjection) config.ProjectionName = LoginNameProjectionTable config.Reducers = p.reducers() config.InitCheck = crdb.NewViewCheck( viewStmt, crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(LoginNameUserIDCol, crdb.ColumnTypeText), crdb.NewColumn(LoginNameUserUserNameCol, crdb.ColumnTypeText), crdb.NewColumn(LoginNameUserResourceOwnerCol, crdb.ColumnTypeText), crdb.NewColumn(LoginNameUserInstanceIDCol, crdb.ColumnTypeText), }, crdb.NewPrimaryKey(LoginNameUserInstanceIDCol, LoginNameUserIDCol), loginNameUserSuffix, crdb.WithIndex(crdb.NewIndex("ro_idx", []string{LoginNameUserResourceOwnerCol})), ), crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(LoginNameDomainNameCol, crdb.ColumnTypeText), crdb.NewColumn(LoginNameDomainIsPrimaryCol, crdb.ColumnTypeBool, crdb.Default(false)), crdb.NewColumn(LoginNameDomainResourceOwnerCol, crdb.ColumnTypeText), crdb.NewColumn(LoginNameDomainInstanceIDCol, crdb.ColumnTypeText), }, crdb.NewPrimaryKey(LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, LoginNameDomainNameCol), loginNameDomainSuffix, ), crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(LoginNamePoliciesMustBeDomainCol, crdb.ColumnTypeBool), crdb.NewColumn(LoginNamePoliciesIsDefaultCol, crdb.ColumnTypeBool), crdb.NewColumn(LoginNamePoliciesResourceOwnerCol, crdb.ColumnTypeText), crdb.NewColumn(LoginNamePoliciesInstanceIDCol, crdb.ColumnTypeText), }, crdb.NewPrimaryKey(LoginNamePoliciesInstanceIDCol, LoginNamePoliciesResourceOwnerCol), loginNamePolicySuffix, crdb.WithIndex(crdb.NewIndex("is_default_idx", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})), ), ) p.StatementHandler = crdb.NewStatementHandler(ctx, config) return p } func (p *loginNameProjection) reducers() []handler.AggregateReducer { return []handler.AggregateReducer{ { Aggregate: user.AggregateType, EventRedusers: []handler.EventReducer{ { Event: user.UserV1AddedType, Reduce: p.reduceUserCreated, }, { Event: user.HumanAddedType, Reduce: p.reduceUserCreated, }, { Event: user.HumanRegisteredType, Reduce: p.reduceUserCreated, }, { Event: user.UserV1RegisteredType, Reduce: p.reduceUserCreated, }, { Event: user.MachineAddedEventType, Reduce: p.reduceUserCreated, }, { Event: user.UserRemovedType, Reduce: p.reduceUserRemoved, }, { Event: user.UserUserNameChangedType, Reduce: p.reduceUserNameChanged, }, { // changes the username of the user // this event occures in orgs // where policy.must_be_domain=false Event: user.UserDomainClaimedType, Reduce: p.reduceUserDomainClaimed, }, }, }, { Aggregate: org.AggregateType, EventRedusers: []handler.EventReducer{ { Event: org.DomainPolicyAddedEventType, Reduce: p.reduceOrgIAMPolicyAdded, }, { Event: org.DomainPolicyChangedEventType, Reduce: p.reduceDomainPolicyChanged, }, { Event: org.DomainPolicyRemovedEventType, Reduce: p.reduceDomainPolicyRemoved, }, { Event: org.OrgDomainPrimarySetEventType, Reduce: p.reducePrimaryDomainSet, }, { Event: org.OrgDomainRemovedEventType, Reduce: p.reduceDomainRemoved, }, { Event: org.OrgDomainVerifiedEventType, Reduce: p.reduceDomainVerified, }, }, }, { Aggregate: instance.AggregateType, EventRedusers: []handler.EventReducer{ { Event: instance.DomainPolicyAddedEventType, Reduce: p.reduceOrgIAMPolicyAdded, }, { Event: instance.DomainPolicyChangedEventType, Reduce: p.reduceDomainPolicyChanged, }, { Event: instance.InstanceRemovedEventType, Reduce: p.reduceInstanceRemoved, }, }, }, } } func (p *loginNameProjection) reduceUserCreated(event eventstore.Event) (*handler.Statement, error) { var userName string switch e := event.(type) { case *user.HumanAddedEvent: userName = e.UserName case *user.HumanRegisteredEvent: userName = e.UserName case *user.MachineAddedEvent: userName = e.UserName default: return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ayo69", "reduce.wrong.event.type %v", []eventstore.EventType{user.UserV1AddedType, user.HumanAddedType, user.UserV1RegisteredType, user.HumanRegisteredType, user.MachineAddedEventType}) } return crdb.NewCreateStatement( event, []handler.Column{ handler.NewCol(LoginNameUserIDCol, event.Aggregate().ID), handler.NewCol(LoginNameUserUserNameCol, userName), handler.NewCol(LoginNameUserResourceOwnerCol, event.Aggregate().ResourceOwner), handler.NewCol(LoginNameUserInstanceIDCol, event.Aggregate().InstanceID), }, crdb.WithTableSuffix(loginNameUserSuffix), ), nil } func (p *loginNameProjection) reduceUserRemoved(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.UserRemovedEvent) if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-QIe3C", "reduce.wrong.event.type %s", user.UserRemovedType) } return crdb.NewDeleteStatement( event, []handler.Condition{ handler.NewCond(LoginNameUserIDCol, e.Aggregate().ID), }, crdb.WithTableSuffix(loginNameUserSuffix), ), nil } func (p *loginNameProjection) reduceUserNameChanged(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.UsernameChangedEvent) if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-QlwjC", "reduce.wrong.event.type %s", user.UserUserNameChangedType) } return crdb.NewUpdateStatement( event, []handler.Column{ handler.NewCol(LoginNameUserUserNameCol, e.UserName), }, []handler.Condition{ handler.NewCond(LoginNameUserIDCol, e.Aggregate().ID), }, crdb.WithTableSuffix(loginNameUserSuffix), ), nil } func (p *loginNameProjection) reduceUserDomainClaimed(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.DomainClaimedEvent) if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-AQMBY", "reduce.wrong.event.type %s", user.UserDomainClaimedType) } return crdb.NewUpdateStatement( event, []handler.Column{ handler.NewCol(LoginNameUserUserNameCol, e.UserName), }, []handler.Condition{ handler.NewCond(LoginNameUserIDCol, e.Aggregate().ID), }, crdb.WithTableSuffix(loginNameUserSuffix), ), nil } func (p *loginNameProjection) reduceOrgIAMPolicyAdded(event eventstore.Event) (*handler.Statement, error) { var ( policyEvent *policy.DomainPolicyAddedEvent isDefault bool ) switch e := event.(type) { case *org.DomainPolicyAddedEvent: policyEvent = &e.DomainPolicyAddedEvent isDefault = false case *instance.DomainPolicyAddedEvent: policyEvent = &e.DomainPolicyAddedEvent isDefault = true default: return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-yCV6S", "reduce.wrong.event.type %v", []eventstore.EventType{org.DomainPolicyAddedEventType, instance.DomainPolicyAddedEventType}) } return crdb.NewCreateStatement( event, []handler.Column{ handler.NewCol(LoginNamePoliciesMustBeDomainCol, policyEvent.UserLoginMustBeDomain), handler.NewCol(LoginNamePoliciesIsDefaultCol, isDefault), handler.NewCol(LoginNamePoliciesResourceOwnerCol, policyEvent.Aggregate().ResourceOwner), handler.NewCol(LoginNamePoliciesInstanceIDCol, policyEvent.Aggregate().InstanceID), }, crdb.WithTableSuffix(loginNamePolicySuffix), ), nil } func (p *loginNameProjection) reduceDomainPolicyChanged(event eventstore.Event) (*handler.Statement, error) { var policyEvent *policy.DomainPolicyChangedEvent switch e := event.(type) { case *org.DomainPolicyChangedEvent: policyEvent = &e.DomainPolicyChangedEvent case *instance.DomainPolicyChangedEvent: policyEvent = &e.DomainPolicyChangedEvent default: return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ArFDd", "reduce.wrong.event.type %v", []eventstore.EventType{org.DomainPolicyChangedEventType, instance.DomainPolicyChangedEventType}) } if policyEvent.UserLoginMustBeDomain == nil { return crdb.NewNoOpStatement(event), nil } return crdb.NewUpdateStatement( event, []handler.Column{ handler.NewCol(LoginNamePoliciesMustBeDomainCol, *policyEvent.UserLoginMustBeDomain), }, []handler.Condition{ handler.NewCond(LoginNamePoliciesResourceOwnerCol, policyEvent.Aggregate().ResourceOwner), }, crdb.WithTableSuffix(loginNamePolicySuffix), ), nil } func (p *loginNameProjection) reduceDomainPolicyRemoved(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*org.DomainPolicyRemovedEvent) if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ysEeB", "reduce.wrong.event.type %s", org.DomainPolicyRemovedEventType) } return crdb.NewDeleteStatement( event, []handler.Condition{ handler.NewCond(LoginNamePoliciesResourceOwnerCol, e.Aggregate().ResourceOwner), }, crdb.WithTableSuffix(loginNamePolicySuffix), ), nil } func (p *loginNameProjection) reduceDomainVerified(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*org.DomainVerifiedEvent) if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-weGAh", "reduce.wrong.event.type %s", org.OrgDomainVerifiedEventType) } return crdb.NewCreateStatement( event, []handler.Column{ handler.NewCol(LoginNameDomainNameCol, e.Domain), handler.NewCol(LoginNameDomainResourceOwnerCol, e.Aggregate().ResourceOwner), handler.NewCol(LoginNameDomainInstanceIDCol, e.Aggregate().InstanceID), }, crdb.WithTableSuffix(loginNameDomainSuffix), ), nil } func (p *loginNameProjection) reducePrimaryDomainSet(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*org.DomainPrimarySetEvent) if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-eOXPN", "reduce.wrong.event.type %s", org.OrgDomainPrimarySetEventType) } return crdb.NewMultiStatement( e, crdb.AddUpdateStatement( []handler.Column{ handler.NewCol(LoginNameDomainIsPrimaryCol, false), }, []handler.Condition{ handler.NewCond(LoginNameDomainResourceOwnerCol, e.Aggregate().ResourceOwner), handler.NewCond(LoginNameDomainIsPrimaryCol, true), }, crdb.WithTableSuffix(loginNameDomainSuffix), ), crdb.AddUpdateStatement( []handler.Column{ handler.NewCol(LoginNameDomainIsPrimaryCol, true), }, []handler.Condition{ handler.NewCond(LoginNameDomainNameCol, e.Domain), handler.NewCond(LoginNameDomainResourceOwnerCol, e.Aggregate().ResourceOwner), }, crdb.WithTableSuffix(loginNameDomainSuffix), ), ), nil } func (p *loginNameProjection) reduceDomainRemoved(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*org.DomainRemovedEvent) if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-4RHYq", "reduce.wrong.event.type %s", org.OrgDomainRemovedEventType) } return crdb.NewDeleteStatement( event, []handler.Condition{ handler.NewCond(LoginNameDomainNameCol, e.Domain), handler.NewCond(LoginNameDomainResourceOwnerCol, e.Aggregate().ResourceOwner), }, crdb.WithTableSuffix(loginNameDomainSuffix), ), nil } func (p *loginNameProjection) reduceInstanceRemoved(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*instance.InstanceRemovedEvent) if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ASeg3", "reduce.wrong.event.type %s", instance.InstanceRemovedEventType) } return crdb.NewMultiStatement( event, crdb.AddDeleteStatement( []handler.Condition{ handler.NewCond(LoginNameDomainInstanceIDCol, e.Aggregate().ID), }, crdb.WithTableSuffix(loginNameDomainSuffix), ), crdb.AddDeleteStatement( []handler.Condition{ handler.NewCond(LoginNamePoliciesInstanceIDCol, e.Aggregate().ID), }, crdb.WithTableSuffix(loginNamePolicySuffix), ), crdb.AddDeleteStatement( []handler.Condition{ handler.NewCond(LoginNameUserInstanceIDCol, e.Aggregate().ID), }, crdb.WithTableSuffix(loginNameUserSuffix), ), ), nil }