Files
zitadel/internal/command/policy_org_model.go
Stefan Benz 6d98b33c56 feat: organization settings for user uniqueness (#10246)
# Which Problems Are Solved

Currently the username uniqueness is on instance level, we want to
achieve a way to set it at organization level.

# How the Problems Are Solved

Addition of endpoints and a resource on organization level, where this
setting can be managed. If nothing it set, the uniqueness is expected to
be at instance level, where only users with instance permissions should
be able to change this setting.

# Additional Changes

None

# Additional Context

Includes #10086
Closes #9964 

---------

Co-authored-by: Marco A. <marco@zitadel.com>
2025-07-29 15:56:21 +02:00

191 lines
5.9 KiB
Go

package command
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
"github.com/zitadel/zitadel/internal/repository/user"
)
type PolicyDomainWriteModel struct {
eventstore.WriteModel
UserLoginMustBeDomain bool
ValidateOrgDomains bool
SMTPSenderAddressMatchesInstanceDomain bool
State domain.PolicyState
}
func (wm *PolicyDomainWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *policy.DomainPolicyAddedEvent:
wm.UserLoginMustBeDomain = e.UserLoginMustBeDomain
wm.ValidateOrgDomains = e.ValidateOrgDomains
wm.SMTPSenderAddressMatchesInstanceDomain = e.SMTPSenderAddressMatchesInstanceDomain
wm.State = domain.PolicyStateActive
case *policy.DomainPolicyChangedEvent:
if e.UserLoginMustBeDomain != nil {
wm.UserLoginMustBeDomain = *e.UserLoginMustBeDomain
}
if e.ValidateOrgDomains != nil {
wm.ValidateOrgDomains = *e.ValidateOrgDomains
}
if e.SMTPSenderAddressMatchesInstanceDomain != nil {
wm.SMTPSenderAddressMatchesInstanceDomain = *e.SMTPSenderAddressMatchesInstanceDomain
}
case *policy.DomainPolicyRemovedEvent:
wm.State = domain.PolicyStateRemoved
}
}
return wm.WriteModel.Reduce()
}
type DomainPolicyUsernamesWriteModel struct {
eventstore.WriteModel
PrimaryDomain string
VerifiedDomains []string
Users []*domainPolicyUsers
}
type domainPolicyUsers struct {
id string
username string
}
func NewDomainPolicyUsernamesWriteModel(orgID string) *DomainPolicyUsernamesWriteModel {
return &DomainPolicyUsernamesWriteModel{
WriteModel: eventstore.WriteModel{
ResourceOwner: orgID,
},
Users: make([]*domainPolicyUsers, 0),
}
}
func (wm *DomainPolicyUsernamesWriteModel) AppendEvents(events ...eventstore.Event) {
wm.WriteModel.AppendEvents(events...)
}
func (wm *DomainPolicyUsernamesWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *org.DomainVerifiedEvent:
wm.VerifiedDomains = append(wm.VerifiedDomains, e.Domain)
case *org.DomainRemovedEvent:
wm.removeDomain(e.Domain)
case *org.DomainPrimarySetEvent:
wm.PrimaryDomain = e.Domain
case *user.HumanAddedEvent:
wm.Users = append(wm.Users, &domainPolicyUsers{id: e.Aggregate().ID, username: e.UserName})
case *user.HumanRegisteredEvent:
wm.Users = append(wm.Users, &domainPolicyUsers{id: e.Aggregate().ID, username: e.UserName})
case *user.MachineAddedEvent:
wm.Users = append(wm.Users, &domainPolicyUsers{id: e.Aggregate().ID, username: e.UserName})
case *user.UsernameChangedEvent:
for _, user := range wm.Users {
if user.id == e.Aggregate().ID {
user.username = e.UserName
break
}
}
case *user.DomainClaimedEvent:
for _, user := range wm.Users {
if user.id == e.Aggregate().ID {
user.username = e.UserName
break
}
}
case *user.UserRemovedEvent:
wm.removeUser(e.Aggregate().ID)
}
}
return wm.WriteModel.Reduce()
}
func (wm *DomainPolicyUsernamesWriteModel) removeDomain(domain string) {
for i, verifiedDomain := range wm.VerifiedDomains {
if verifiedDomain == domain {
wm.VerifiedDomains[i] = wm.VerifiedDomains[len(wm.VerifiedDomains)-1]
wm.VerifiedDomains[len(wm.VerifiedDomains)-1] = ""
wm.VerifiedDomains = wm.VerifiedDomains[:len(wm.VerifiedDomains)-1]
return
}
}
}
func (wm *DomainPolicyUsernamesWriteModel) removeUser(userID string) {
for i, user := range wm.Users {
if user.id == userID {
wm.Users[i] = wm.Users[len(wm.Users)-1]
wm.Users[len(wm.Users)-1] = nil
wm.Users = wm.Users[:len(wm.Users)-1]
return
}
}
}
func (wm *DomainPolicyUsernamesWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(org.AggregateType, user.AggregateType).
EventTypes(
org.OrgDomainVerifiedEventType,
org.OrgDomainRemovedEventType,
org.OrgDomainPrimarySetEventType,
user.HumanAddedType,
user.HumanRegisteredType,
user.MachineAddedEventType,
user.UserUserNameChangedType,
user.UserDomainClaimedType,
user.UserRemovedType,
).
Builder()
}
func (wm *DomainPolicyUsernamesWriteModel) NewUsernameChangedEvents(ctx context.Context, userLoginMustBeDomain, organizationScopedUsernames, oldUserLoginMustBeDomain bool) []eventstore.Command {
events := make([]eventstore.Command, 0, len(wm.Users))
for _, changeUser := range wm.Users {
events = append(events, user.NewUsernameChangedEvent(ctx,
&user.NewAggregate(changeUser.id, wm.ResourceOwner).Aggregate,
changeUser.username,
wm.newUsername(changeUser.username, userLoginMustBeDomain),
userLoginMustBeDomain,
organizationScopedUsernames,
user.UsernameChangedEventWithPolicyChange(oldUserLoginMustBeDomain),
))
}
return events
}
func (wm *DomainPolicyUsernamesWriteModel) Usernames() []string {
usernames := make([]string, 0, len(wm.Users))
for i, user := range wm.Users {
usernames[i] = user.username
}
return usernames
}
func (wm *DomainPolicyUsernamesWriteModel) newUsername(username string, userLoginMustBeDomain bool) string {
if !userLoginMustBeDomain {
// if the UserLoginMustBeDomain will be false, then it's currently true
// which means the usernames must be suffixed to ensure their uniqueness
// and the preferred login name remains the same
return username + "@" + wm.PrimaryDomain
}
// the UserLoginMustBeDomain is currently false
// which means the usernames might already be suffixed by a verified domain
// so let's remove a potential duplicate suffix
for _, verifiedDomain := range wm.VerifiedDomains {
if index := strings.LastIndex(username, "@"+verifiedDomain); index > 0 {
return username[:index]
}
}
return username
}