zitadel/internal/command/user_v2_human.go
Livio Spring d016379e2a
feat: pass and handle auth request context for email links (#7815)
* pass and handle auth request context

* tests and cleanup

* cleanup
2024-04-24 17:50:58 +02:00

461 lines
13 KiB
Go

package command
import (
"context"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type ChangeHuman struct {
ID string
Username *string
Profile *Profile
Email *Email
Phone *Phone
Password *Password
// 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 Profile struct {
FirstName *string
LastName *string
NickName *string
DisplayName *string
PreferredLanguage *language.Tag
Gender *domain.Gender
}
type Password struct {
// Either you have to have permission, a password code or the old password to change
PasswordCode *string
OldPassword *string
Password *string
EncodedPasswordHash *string
ChangeRequired bool
}
func (h *ChangeHuman) Validate(hasher *crypto.Hasher) (err error) {
if h.Email != nil && h.Email.Address != "" {
if err := h.Email.Validate(); err != nil {
return err
}
}
if h.Phone != nil && h.Phone.Number != "" {
if h.Phone.Number, err = h.Phone.Number.Normalize(); err != nil {
return err
}
}
if h.Password != nil {
if err := h.Password.Validate(hasher); err != nil {
return err
}
}
return nil
}
func (p *Password) Validate(hasher *crypto.Hasher) error {
if p.EncodedPasswordHash != nil {
if !hasher.EncodingSupported(*p.EncodedPasswordHash) {
return zerrors.ThrowInvalidArgument(nil, "USER-oz74onzvqr", "Errors.User.Password.NotSupported")
}
}
if p.Password == nil && p.EncodedPasswordHash == nil {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-3klek4sbns", "Errors.User.Password.Empty")
}
return nil
}
func (h *ChangeHuman) Changed() bool {
if h.Username != nil {
return true
}
if h.Profile != nil {
return true
}
if h.Email != nil {
return true
}
if h.Phone != nil {
return true
}
if h.Password != nil {
return true
}
return false
}
func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool, alg crypto.EncryptionAlgorithm) (err error) {
if resourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-095xh8fll1", "Errors.Internal")
}
if err := human.Validate(c.userPasswordHasher); err != nil {
return err
}
if human.ID == "" {
human.ID, err = c.idGenerator.Next()
if err != nil {
return err
}
}
// only check if user is already existing
existingHuman, err := c.userExistsWriteModel(
ctx,
human.ID,
)
if err != nil {
return err
}
if isUserStateExists(existingHuman.UserState) {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-7yiox1isql", "Errors.User.AlreadyExisting")
}
// check for permission to create user on resourceOwner
if !human.Register {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, human.ID); err != nil {
return err
}
}
// add resourceowner for the events with the aggregate
existingHuman.ResourceOwner = resourceOwner
domainPolicy, err := c.domainPolicyWriteModel(ctx, resourceOwner)
if err != nil {
return err
}
if err = c.userValidateDomain(ctx, resourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil {
return err
}
var createCmd humanCreationCommand
if human.Register {
createCmd = user.NewHumanRegisteredEvent(
ctx,
&existingHuman.Aggregate().Aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLanguage,
human.Gender,
human.Email.Address,
domainPolicy.UserLoginMustBeDomain,
human.UserAgentID,
)
} else {
createCmd = user.NewHumanAddedEvent(
ctx,
&existingHuman.Aggregate().Aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLanguage,
human.Gender,
human.Email.Address,
domainPolicy.UserLoginMustBeDomain,
)
}
if human.Phone.Number != "" {
createCmd.AddPhoneData(human.Phone.Number)
}
// separated to change when old user logic is not used anymore
filter := c.eventstore.Filter //nolint:staticcheck
if err := addHumanCommandPassword(ctx, filter, createCmd, human, c.userPasswordHasher); err != nil {
return err
}
cmds := make([]eventstore.Command, 0, 3)
cmds = append(cmds, createCmd)
cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail)
if err != nil {
return err
}
cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, existingHuman.Aggregate(), human, alg)
if err != nil {
return err
}
for _, metadataEntry := range human.Metadata {
cmds = append(cmds, user.NewMetadataSetEvent(
ctx,
&existingHuman.Aggregate().Aggregate,
metadataEntry.Key,
metadataEntry.Value,
))
}
for _, link := range human.Links {
cmd, err := addLink(ctx, filter, existingHuman.Aggregate(), link)
if err != nil {
return err
}
cmds = append(cmds, cmd)
}
if len(cmds) == 0 {
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
}
err = c.pushAppendAndReduce(ctx, existingHuman, cmds...)
if err != nil {
return err
}
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
}
func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg crypto.EncryptionAlgorithm) (err error) {
if err := human.Validate(c.userPasswordHasher); err != nil {
return err
}
existingHuman, err := c.userHumanWriteModel(
ctx,
human.ID,
human.Profile != nil,
human.Email != nil,
human.Phone != nil,
human.Password != nil,
false, // avatar not updateable
false, // IDPLinks not updateable
)
if err != nil {
return err
}
if !isUserStateExists(existingHuman.UserState) {
return zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound")
}
if human.Changed() {
if err := c.checkPermissionUpdateUser(ctx, existingHuman.ResourceOwner, existingHuman.AggregateID); err != nil {
return err
}
}
cmds := make([]eventstore.Command, 0)
if human.Username != nil {
cmds, err = c.changeUsername(ctx, cmds, existingHuman, *human.Username)
if err != nil {
return err
}
}
if human.Profile != nil {
cmds, err = changeUserProfile(ctx, cmds, existingHuman, human.Profile)
if err != nil {
return err
}
}
if human.Email != nil {
cmds, human.EmailCode, err = c.changeUserEmail(ctx, cmds, existingHuman, human.Email, alg)
if err != nil {
return err
}
}
if human.Phone != nil {
cmds, human.PhoneCode, err = c.changeUserPhone(ctx, cmds, existingHuman, human.Phone, alg)
if err != nil {
return err
}
}
if human.Password != nil {
cmds, err = c.changeUserPassword(ctx, cmds, existingHuman, human.Password, alg)
if err != nil {
return err
}
}
if len(cmds) == 0 {
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
}
err = c.pushAppendAndReduce(ctx, existingHuman, cmds...)
if err != nil {
return err
}
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
}
func (c *Commands) changeUserEmail(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, email *Email, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, code *string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if email.Address != "" && email.Address != wm.Email {
cmds = append(cmds, user.NewHumanEmailChangedEvent(ctx, &wm.Aggregate().Aggregate, email.Address))
if email.Verified {
return append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), code, nil
} else {
cryptoCode, err := c.newEmailCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
if err != nil {
return cmds, code, err
}
cmds = append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &wm.Aggregate().Aggregate, cryptoCode.Crypted, cryptoCode.Expiry, email.URLTemplate, email.ReturnCode))
if email.ReturnCode {
code = &cryptoCode.Plain
}
return cmds, code, nil
}
}
// only create separate event of verified if email was not changed
if email.Verified && wm.IsEmailVerified != email.Verified {
return append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), nil, nil
}
return cmds, code, nil
}
func (c *Commands) changeUserPhone(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, phone *Phone, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, code *string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if phone.Number != "" && phone.Number != wm.Phone {
cmds = append(cmds, user.NewHumanPhoneChangedEvent(ctx, &wm.Aggregate().Aggregate, phone.Number))
if phone.Verified {
return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), code, nil
} else {
cryptoCode, err := c.newPhoneCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
if err != nil {
return cmds, code, err
}
cmds = append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &wm.Aggregate().Aggregate, cryptoCode.Crypted, cryptoCode.Expiry, phone.ReturnCode))
if phone.ReturnCode {
code = &cryptoCode.Plain
}
return cmds, code, nil
}
}
// only create separate event of verified if email was not changed
if phone.Verified && wm.IsPhoneVerified != phone.Verified {
return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &wm.Aggregate().Aggregate)), code, nil
}
return cmds, code, nil
}
func changeUserProfile(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, profile *Profile) ([]eventstore.Command, error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
cmd, err := wm.NewProfileChangedEvent(ctx, profile.FirstName, profile.LastName, profile.NickName, profile.DisplayName, profile.PreferredLanguage, profile.Gender)
if cmd != nil {
return append(cmds, cmd), err
}
return cmds, err
}
func (c *Commands) changeUserPassword(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, password *Password, alg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
// Either have a code to set the password
if password.PasswordCode != nil {
if err := crypto.VerifyCode(wm.PasswordCodeCreationDate, wm.PasswordCodeExpiry, wm.PasswordCode, *password.PasswordCode, alg); err != nil {
return cmds, err
}
}
var encodedPassword string
// or have the old password to change it
if password.OldPassword != nil {
// newly encode old password if no new and already encoded password is set
pw := *password.OldPassword
if password.Password != nil {
pw = *password.Password
}
alreadyEncodedPassword, err := c.verifyAndUpdatePassword(ctx, wm.PasswordEncodedHash, *password.OldPassword, pw)
if err != nil {
return cmds, err
}
encodedPassword = alreadyEncodedPassword
}
// password already hashed in request
if password.EncodedPasswordHash != nil {
cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.EncodedPasswordHash, "", password.ChangeRequired, true)
if cmd != nil {
return append(cmds, cmd), err
}
return cmds, err
}
// password already hashed in verify
if encodedPassword != "" {
cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, encodedPassword, "", password.ChangeRequired, true)
if cmd != nil {
return append(cmds, cmd), err
}
return cmds, err
}
// password still to be hashed
if password.Password != nil {
cmd, err := c.setPasswordCommand(ctx, &wm.Aggregate().Aggregate, wm.UserState, *password.Password, "", password.ChangeRequired, false)
if cmd != nil {
return append(cmds, cmd), err
}
return cmds, err
}
// no password changes necessary
return cmds, nil
}
func (c *Commands) userExistsWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewUserExistsWriteModel(userID, "")
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) userHumanWriteModel(ctx context.Context, userID string, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM bool) (writeModel *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewUserHumanWriteModel(userID, "", profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) orgDomainVerifiedWriteModel(ctx context.Context, domain string) (writeModel *OrgDomainVerifiedWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewOrgDomainVerifiedWriteModel(domain)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}