Files
zitadel/internal/command/user_human.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

813 lines
26 KiB
Go

package command
import (
"context"
"strings"
"time"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/notification/senders"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) (*domain.Human, error) {
human, err := c.getHumanWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return nil, err
}
if !isUserStateExists(human.UserState) {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-M9dsd", "Errors.User.NotFound")
}
return writeModelToHuman(human), nil
}
type AddHuman struct {
// ID is optional, if empty it will be generated
ID string
// Username is required
Username string
// FirstName is required
FirstName string
// LastName is required
LastName string
// NickName is required
NickName string
// DisplayName is required
DisplayName string
// Email is required
Email Email
// PreferredLanguage is required
PreferredLanguage language.Tag
// Gender is required
Gender domain.Gender
// Phone represents an international phone number
Phone Phone
// Password is optional
Password string
// EncodedPasswordHash is optional
EncodedPasswordHash string
// PasswordChangeRequired is used if the `Password`-field is set
PasswordChangeRequired bool
Passwordless bool
ExternalIDP bool
Register bool
// SetInactive whether the user initially should be set as inactive
SetInactive bool
// UserAgentID is optional and can be passed in case the user registered themselves.
// This will be used in the login UI to handle authentication automatically.
UserAgentID string
// AuthRequestID is optional and can be passed in case the user registered themselves.
// This will be used to pass the information in notifications for links to the login UI.
AuthRequestID string
Metadata []*AddMetadataEntry
// Links are optional
Links []*AddLink
// TOTPSecret is optional
TOTPSecret string
// Details are set after a successful execution of the command
Details *domain.ObjectDetails
// EmailCode is set by the command
EmailCode *string
// PhoneCode is set by the command
PhoneCode *string
}
type AddLink struct {
IDPID string
DisplayName string
IDPExternalID string
}
func (h *AddHuman) Validate(hasher *crypto.Hasher) (err error) {
if err := h.Email.Validate(); err != nil {
return err
}
if h.Username = strings.TrimSpace(h.Username); h.Username == "" {
return zerrors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
}
if h.FirstName = strings.TrimSpace(h.FirstName); h.FirstName == "" {
return zerrors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
}
if h.LastName = strings.TrimSpace(h.LastName); h.LastName == "" {
return zerrors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
}
h.ensureDisplayName()
if h.Phone.Number != "" {
if h.Phone.Number, err = h.Phone.Number.Normalize(); err != nil {
return err
}
}
for _, metadataEntry := range h.Metadata {
if err := metadataEntry.Valid(); err != nil {
return err
}
}
if h.EncodedPasswordHash != "" {
if !hasher.EncodingSupported(h.EncodedPasswordHash) {
return zerrors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported")
}
}
return nil
}
type AddMetadataEntry struct {
Key string
Value []byte
}
func (m *AddMetadataEntry) Valid() error {
if m.Key = strings.TrimSpace(m.Key); m.Key == "" {
return zerrors.ThrowInvalidArgument(nil, "USER-Drght", "Errors.User.Metadata.KeyEmpty")
}
if len(m.Value) == 0 {
return zerrors.ThrowInvalidArgument(nil, "USER-Dbgth", "Errors.User.Metadata.ValueEmpty")
}
return nil
}
// Deprecated: use commands.AddUserHuman
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool) (err error) {
if resourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
}
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
c.AddHumanCommand(
human,
resourceOwner,
c.userPasswordHasher,
c.userEncryption,
allowInitMail,
))
if err != nil {
return err
}
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return err
}
human.Details = &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreatedAt(),
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
}
return nil
}
type humanCreationCommand interface {
eventstore.Command
AddPhoneData(phoneNumber domain.PhoneNumber)
AddPasswordData(encoded string, changeRequired bool)
}
//nolint:gocognit
func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, hasher *crypto.Hasher, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if err := human.Validate(hasher); err != nil {
return nil, err
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if err := c.addHumanCommandCheckID(ctx, filter, human, orgID); err != nil {
return nil, err
}
a := user.NewAggregate(human.ID, orgID)
domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ResourceOwner)
if err != nil {
return nil, err
}
if err = c.userValidateDomain(ctx, a.ResourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil {
return nil, err
}
organizationScopedUsername, err := checkOrganizationScopedUsernames(ctx, filter, a.ResourceOwner, nil)
if err != nil {
return nil, err
}
var createCmd humanCreationCommand
if human.Register {
createCmd = user.NewHumanRegisteredEvent(
ctx,
&a.Aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLanguage,
human.Gender,
human.Email.Address,
domainPolicy.UserLoginMustBeDomain || organizationScopedUsername,
"", // no user agent id available
)
} else {
createCmd = user.NewHumanAddedEvent(
ctx,
&a.Aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLanguage,
human.Gender,
human.Email.Address,
domainPolicy.UserLoginMustBeDomain || organizationScopedUsername,
)
}
if human.Phone.Number != "" {
createCmd.AddPhoneData(human.Phone.Number)
}
if err := addHumanCommandPassword(ctx, filter, createCmd, human, hasher); err != nil {
return nil, err
}
cmds := make([]eventstore.Command, 0, 3)
cmds = append(cmds, createCmd)
cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, a, human, codeAlg, allowInitMail)
if err != nil {
return nil, err
}
cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, a, human, codeAlg)
if err != nil {
return nil, err
}
for _, metadataEntry := range human.Metadata {
cmds = append(cmds, user.NewMetadataSetEvent(
ctx,
&a.Aggregate,
metadataEntry.Key,
metadataEntry.Value,
))
}
for _, link := range human.Links {
cmd, err := addLink(ctx, filter, a, link)
if err != nil {
return nil, err
}
cmds = append(cmds, cmd)
}
return cmds, nil
}, nil
}
}
func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) ([]eventstore.Command, error) {
if human.Email.Verified {
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
}
// if allowInitMail, used for v1 api (system, admin, mgmt, auth):
// add init code if
// email not verified or
// user not registered and password set
if allowInitMail && human.shouldAddInitCode() {
initCode, err := c.newUserInitCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry, human.AuthRequestID)), nil
}
if human.Email.NoEmailVerification {
return cmds, nil
}
if !human.Email.Verified {
emailCode, err := c.newEmailCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
if human.Email.ReturnCode {
human.EmailCode = &emailCode.Plain
}
return append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &a.Aggregate, emailCode.Crypted, emailCode.Expiry, human.Email.URLTemplate, human.Email.ReturnCode, human.AuthRequestID)), nil
}
return cmds, nil
}
func addLink(ctx context.Context, filter preparation.FilterToQueryReducer, a *user.Aggregate, link *AddLink) (eventstore.Command, error) {
exists, err := ExistsIDPOnOrgOrInstance(ctx, filter, authz.GetInstance(ctx).InstanceID(), a.ResourceOwner, link.IDPID)
if !exists || err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-39nf2", "Errors.IDPConfig.NotExisting")
}
return user.NewUserIDPLinkAddedEvent(ctx, &a.Aggregate, link.IDPID, link.DisplayName, link.IDPExternalID), nil
}
func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
if human.Phone.Number == "" {
return cmds, nil
}
if human.Phone.Verified {
return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)), nil
}
phoneCode, generatorID, err := c.newPhoneCode(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, codeAlg, c.defaultSecretGenerators.PhoneVerificationCode)
if err != nil {
return nil, err
}
if human.Phone.ReturnCode {
human.PhoneCode = &phoneCode.Plain
}
return append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &a.Aggregate, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), human.Phone.ReturnCode, generatorID)), nil
}
// Deprecated: use commands.NewUserHumanWriteModel, to remove deprecated eventstore.Filter
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, human *AddHuman, orgID string) (err error) {
if human.ID == "" {
human.ID, err = c.idGenerator.Next()
if err != nil {
return err
}
}
existingHuman, err := humanWriteModelByID(ctx, filter, human.ID, orgID)
if err != nil {
return err
}
if isUserStateExists(existingHuman.UserState) {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
}
return nil
}
func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, hasher *crypto.Hasher) (err error) {
if human.Password != "" {
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
return err
}
_, spanHash := tracing.NewNamedSpan(ctx, "passwap.Hash")
encodedHash, err := hasher.Hash(human.Password)
spanHash.EndWithError(err)
if err != nil {
return err
}
createCmd.AddPasswordData(encodedHash, human.PasswordChangeRequired)
return nil
}
if human.EncodedPasswordHash != "" {
createCmd.AddPasswordData(human.EncodedPasswordHash, human.PasswordChangeRequired)
}
return nil
}
func (c *Commands) userValidateDomain(ctx context.Context, resourceOwner string, username string, mustBeDomain bool) (err error) {
if mustBeDomain {
return nil
}
index := strings.LastIndex(username, "@")
if index < 0 {
return nil
}
domainCheck, err := c.searchOrgDomainVerifiedByDomain(ctx, username[index+1:])
if err != nil {
return err
}
if domainCheck.Verified && domainCheck.OrgID != resourceOwner {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")
}
return nil
}
func humanValidatePassword(ctx context.Context, filter preparation.FilterToQueryReducer, password string) error {
passwordComplexity, err := passwordComplexityPolicyWriteModel(ctx, filter)
if err != nil {
return err
}
return passwordComplexity.Validate(password)
}
func (h *AddHuman) ensureDisplayName() {
if strings.TrimSpace(h.DisplayName) != "" {
return
}
if strings.TrimSpace(h.FirstName) != "" && strings.TrimSpace(h.LastName) != "" {
h.DisplayName = h.FirstName + " " + h.LastName
return
}
if strings.TrimSpace(string(h.Email.Address)) != "" {
h.DisplayName = string(h.Email.Address)
return
}
h.DisplayName = h.Username
}
// shouldAddInitCode returns true for all added Humans which:
// - were not added from an external IDP
// - and either:
// - have no verified email
// and / or
// - have no authentication method (password / passwordless)
func (h *AddHuman) shouldAddInitCode() bool {
return len(h.Links) == 0 &&
(!h.Email.Verified ||
(!h.Passwordless && h.Password == ""))
}
// Deprecated: use commands.AddUserHuman
func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, state *domain.UserState, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if orgID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
}
domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID)
if err != nil {
return nil, nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.DomainPolicy.NotFound")
}
organizationScopedUsername, err := c.checkOrganizationScopedUsernames(ctx, orgID)
if err != nil {
return nil, nil, err
}
pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID)
if err != nil {
return nil, nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexityPolicy.NotFound")
}
if human.AggregateID != "" {
existing, err := c.getHumanWriteModelByID(ctx, human.AggregateID, human.ResourceOwner)
if err != nil {
return nil, nil, err
}
if existing.UserState.Exists() {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-ziuna", "Errors.User.AlreadyExisting")
}
}
events, userAgg, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, organizationScopedUsername, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator)
if err != nil {
return nil, nil, err
}
if state != nil {
var event eventstore.Command
switch *state {
case domain.UserStateInactive:
event = user.NewUserDeactivatedEvent(ctx, userAgg)
case domain.UserStateLocked:
event = user.NewUserLockedEvent(ctx, userAgg)
case domain.UserStateDeleted:
// users are never imported if deleted
case domain.UserStateActive:
// added because of the linter
case domain.UserStateSuspend:
// added because of the linter
case domain.UserStateInitial:
// added because of the linter
case domain.UserStateUnspecified:
// added because of the linter
}
if event != nil {
events = append(events, event)
}
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
return nil, nil, err
}
err = AppendAndReduce(addedHuman, pushedEvents...)
if err != nil {
return nil, nil, err
}
if addedCode != nil {
err = AppendAndReduce(addedCode, pushedEvents...)
if err != nil {
return nil, nil, err
}
passwordlessCode = writeModelToPasswordlessInitCode(addedCode, code)
}
return writeModelToHuman(addedHuman), passwordlessCode, nil
}
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, orgScopedUsername bool, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if orgID == "" {
return nil, nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty")
}
if err = human.Normalize(); err != nil {
return nil, nil, nil, nil, "", err
}
events, userAgg, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, orgScopedUsername, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
if err != nil {
return nil, nil, nil, nil, "", err
}
if passwordless {
var codeEvent eventstore.Command
codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator)
if err != nil {
return nil, nil, nil, nil, "", err
}
events = append(events, codeEvent)
}
return events, userAgg, humanWriteModel, passwordlessCodeWriteModel, code, nil
}
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, orgScopedUsername bool, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, addedHuman *HumanWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = human.CheckDomainPolicy(domainPolicy); err != nil {
return nil, nil, nil, err
}
human.Username = strings.TrimSpace(human.Username)
human.EmailAddress = human.EmailAddress.Normalize()
if err = c.userValidateDomain(ctx, orgID, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil {
return nil, nil, nil, err
}
if human.AggregateID == "" {
userID, err := c.idGenerator.Next()
if err != nil {
return nil, nil, nil, err
}
human.AggregateID = userID
}
human.EnsureDisplayName()
if human.Password != nil {
if err := human.HashPasswordIfExisting(ctx, pwPolicy, c.userPasswordHasher, human.Password.ChangeRequired); err != nil {
return nil, nil, nil, err
}
}
addedHuman = NewHumanWriteModel(human.AggregateID, orgID)
// TODO: adlerhurst maybe we could simplify the code below
userAgg = UserAggregateFromWriteModelCtx(ctx, &addedHuman.WriteModel)
events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain, orgScopedUsername))
for _, link := range links {
event, err := c.addUserIDPLink(ctx, userAgg, link, false)
if err != nil {
return nil, nil, nil, err
}
events = append(events, event)
}
if human.IsInitialState(passwordless, len(links) > 0) {
initCode, err := domain.NewInitUserCode(initCodeGenerator)
if err != nil {
return nil, nil, nil, err
}
events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry, ""))
} else {
if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified {
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
} else {
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
if err != nil {
return nil, nil, nil, err
}
events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, ""))
}
}
if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified {
phoneCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck
if err != nil {
return nil, nil, nil, err
}
events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), generatorID))
} else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified {
events = append(events, user.NewHumanPhoneVerifiedEvent(ctx, userAgg))
}
return events, userAgg, addedHuman, nil
}
func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) {
if userID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-2xpX9", "Errors.User.UserIDMissing")
}
existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
if !isUserStateExists(existingHuman.UserState) {
return zerrors.ThrowNotFound(nil, "COMMAND-m9cV8", "Errors.User.NotFound")
}
_, err = c.eventstore.Push(ctx,
user.NewHumanMFAInitSkippedEvent(ctx, UserAggregateFromWriteModel(&existingHuman.WriteModel)))
return err
}
// TODO: adlerhurst maybe we can simplify createAddHumanEvent and createRegisterHumanEvent
func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, human *domain.Human, userLoginMustBeDomain, orgScopedUsername bool) *user.HumanAddedEvent {
addEvent := user.NewHumanAddedEvent(
ctx,
aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLanguage,
human.Gender,
human.EmailAddress,
userLoginMustBeDomain || orgScopedUsername,
)
if human.Phone != nil {
addEvent.AddPhoneData(human.PhoneNumber)
}
if human.Address != nil {
addEvent.AddAddressData(
human.Country,
human.Locality,
human.PostalCode,
human.Region,
human.StreetAddress)
}
if human.Password != nil {
addEvent.AddPasswordData(human.Password.EncodedSecret, human.Password.ChangeRequired)
}
if human.HashedPassword != "" {
addEvent.AddPasswordData(human.HashedPassword, false)
}
return addEvent
}
type HumanSignOutSession struct {
ID string
UserID string
}
func (c *Commands) HumansSignOut(ctx context.Context, agentID string, sessions []HumanSignOutSession) error {
if agentID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing")
}
if len(sessions) == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-M0od3", "Errors.User.UserIDMissing")
}
events := make([]eventstore.Command, 0)
for _, session := range sessions {
existingUser, err := c.getHumanWriteModelByID(ctx, session.UserID, "")
if err != nil {
return err
}
if !isUserStateExists(existingUser.UserState) {
continue
}
events = append(events, user.NewHumanSignedOutEvent(
ctx,
UserAggregateFromWriteModel(&existingUser.WriteModel),
agentID,
session.ID,
))
}
if len(events) == 0 {
return nil
}
_, err := c.eventstore.Push(ctx, events...)
return err
}
func (c *Commands) getHumanWriteModelByID(ctx context.Context, userID, resourceowner string) (_ *HumanWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
humanWriteModel := NewHumanWriteModel(userID, resourceowner)
err = c.eventstore.FilterToQueryReducer(ctx, humanWriteModel)
if err != nil {
return nil, err
}
return humanWriteModel, nil
}
func humanWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceowner string) (*HumanWriteModel, error) {
humanWriteModel := NewHumanWriteModel(userID, resourceowner)
events, err := filter(ctx, humanWriteModel.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return humanWriteModel, nil
}
humanWriteModel.AppendEvents(events...)
err = humanWriteModel.Reduce()
return humanWriteModel, err
}
func AddHumanFromDomain(user *domain.Human, metadataList []*domain.Metadata, authRequest *domain.AuthRequest, idp *domain.UserIDPLink) *AddHuman {
addMetadata := make([]*AddMetadataEntry, len(metadataList))
for i, metadata := range metadataList {
addMetadata[i] = &AddMetadataEntry{
Key: metadata.Key,
Value: metadata.Value,
}
}
human := new(AddHuman)
if user.Profile != nil {
human.Username = user.Username
human.FirstName = user.FirstName
human.LastName = user.LastName
human.NickName = user.NickName
human.DisplayName = user.DisplayName
human.PreferredLanguage = user.PreferredLanguage
human.Gender = user.Gender
human.Register = true
human.Metadata = addMetadata
}
if authRequest != nil {
human.UserAgentID = authRequest.AgentID
human.AuthRequestID = authRequest.ID
}
if user.Email != nil {
human.Email = Email{
Address: user.Email.EmailAddress,
Verified: user.Email.IsEmailVerified,
}
}
if user.Phone != nil {
human.Phone = Phone{
Number: user.Phone.PhoneNumber,
Verified: user.Phone.IsPhoneVerified,
}
}
if user.Password != nil {
human.Password = user.Password.SecretString
}
if idp != nil {
human.Links = []*AddLink{
{
IDPID: idp.IDPConfigID,
DisplayName: idp.DisplayName,
IDPExternalID: idp.ExternalUserID,
},
}
}
if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
human.Username = string(human.Email.Address)
}
return human
}
func verifyCode(
ctx context.Context,
codeCreationDate time.Time,
codeExpiry time.Duration,
encryptedCode *crypto.CryptoValue,
codeProviderID string,
codeVerificationID string,
code string,
codeAlg crypto.EncryptionAlgorithm,
getCodeVerifier func(ctx context.Context, id string) (_ senders.CodeGenerator, err error),
) (err error) {
if codeProviderID == "" {
if encryptedCode == nil {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
}
_, spanCrypto := tracing.NewNamedSpan(ctx, "crypto.VerifyCode")
defer func() {
spanCrypto.EndWithError(err)
}()
return crypto.VerifyCode(codeCreationDate, codeExpiry, encryptedCode, code, codeAlg)
}
if getCodeVerifier == nil {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-M0g95", "Errors.User.Code.NotConfigured")
}
verifier, err := getCodeVerifier(ctx, codeProviderID)
if err != nil {
return err
}
return verifier.VerifyCode(codeVerificationID, code)
}