zitadel/internal/command/user_v3_model.go

684 lines
19 KiB
Go
Raw Normal View History

package command
import (
"bytes"
"context"
"encoding/json"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
domain_schema "github.com/zitadel/zitadel/internal/domain/schema"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user/schemauser"
"github.com/zitadel/zitadel/internal/zerrors"
)
type UserV3WriteModel struct {
eventstore.WriteModel
PhoneWM bool
EmailWM bool
DataWM bool
SchemaID string
SchemaRevision uint64
Email string
IsEmailVerified bool
EmailVerifiedFailedCount int
EmailCode *VerifyCode
Phone string
IsPhoneVerified bool
PhoneVerifiedFailedCount int
PhoneCode *VerifyCode
Data json.RawMessage
Locked bool
State domain.UserState
checkPermission domain.PermissionCheck
writePermissionCheck bool
}
func (wm *UserV3WriteModel) GetWriteModel() *eventstore.WriteModel {
return &wm.WriteModel
}
type VerifyCode struct {
Code *crypto.CryptoValue
CreationDate time.Time
Expiry time.Duration
}
func NewExistsUserV3WriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
PhoneWM: false,
EmailWM: false,
DataWM: false,
checkPermission: checkPermission,
}
}
func NewUserV3WriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
PhoneWM: true,
EmailWM: true,
DataWM: true,
checkPermission: checkPermission,
}
}
func NewUserV3EmailWriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
EmailWM: true,
checkPermission: checkPermission,
}
}
func NewUserV3PhoneWriteModel(resourceOwner, userID string, checkPermission domain.PermissionCheck) *UserV3WriteModel {
return &UserV3WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
PhoneWM: true,
checkPermission: checkPermission,
}
}
func (wm *UserV3WriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *schemauser.CreatedEvent:
wm.SchemaID = e.SchemaID
wm.SchemaRevision = e.SchemaRevision
wm.Data = e.Data
wm.Locked = false
wm.State = domain.UserStateActive
case *schemauser.UpdatedEvent:
if e.SchemaID != nil {
wm.SchemaID = *e.SchemaID
}
if e.SchemaRevision != nil {
wm.SchemaRevision = *e.SchemaRevision
}
if len(e.Data) > 0 {
wm.Data = e.Data
}
case *schemauser.DeletedEvent:
wm.State = domain.UserStateDeleted
case *schemauser.EmailUpdatedEvent:
wm.Email = string(e.EmailAddress)
wm.IsEmailVerified = false
wm.EmailVerifiedFailedCount = 0
wm.EmailCode = nil
case *schemauser.EmailCodeAddedEvent:
wm.IsEmailVerified = false
wm.EmailVerifiedFailedCount = 0
wm.EmailCode = &VerifyCode{
Code: e.Code,
CreationDate: e.CreationDate(),
Expiry: e.Expiry,
}
case *schemauser.EmailVerifiedEvent:
wm.IsEmailVerified = true
wm.EmailVerifiedFailedCount = 0
wm.EmailCode = nil
case *schemauser.EmailVerificationFailedEvent:
wm.EmailVerifiedFailedCount += 1
case *schemauser.PhoneUpdatedEvent:
wm.Phone = string(e.PhoneNumber)
wm.IsPhoneVerified = false
wm.PhoneVerifiedFailedCount = 0
wm.EmailCode = nil
case *schemauser.PhoneCodeAddedEvent:
wm.IsPhoneVerified = false
wm.PhoneVerifiedFailedCount = 0
wm.PhoneCode = &VerifyCode{
Code: e.Code,
CreationDate: e.CreationDate(),
Expiry: e.Expiry,
}
case *schemauser.PhoneVerifiedEvent:
wm.PhoneVerifiedFailedCount = 0
wm.IsPhoneVerified = true
wm.PhoneCode = nil
case *schemauser.PhoneVerificationFailedEvent:
wm.PhoneVerifiedFailedCount += 1
case *schemauser.LockedEvent:
wm.Locked = true
case *schemauser.UnlockedEvent:
wm.Locked = false
case *schemauser.DeactivatedEvent:
wm.State = domain.UserStateInactive
case *schemauser.ActivatedEvent:
wm.State = domain.UserStateActive
}
}
return wm.WriteModel.Reduce()
}
func (wm *UserV3WriteModel) Query() *eventstore.SearchQueryBuilder {
builder := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent)
if wm.ResourceOwner != "" {
builder = builder.ResourceOwner(wm.ResourceOwner)
}
eventtypes := []eventstore.EventType{
schemauser.CreatedType,
schemauser.DeletedType,
schemauser.ActivatedType,
schemauser.DeactivatedType,
schemauser.LockedType,
schemauser.UnlockedType,
}
if wm.DataWM {
eventtypes = append(eventtypes,
schemauser.UpdatedType,
)
}
if wm.EmailWM {
eventtypes = append(eventtypes,
schemauser.EmailUpdatedType,
schemauser.EmailVerifiedType,
schemauser.EmailCodeAddedType,
schemauser.EmailVerificationFailedType,
)
}
if wm.PhoneWM {
eventtypes = append(eventtypes,
schemauser.PhoneUpdatedType,
schemauser.PhoneVerifiedType,
schemauser.PhoneCodeAddedType,
schemauser.PhoneVerificationFailedType,
)
}
return builder.AddQuery().
AggregateTypes(schemauser.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(eventtypes...).Builder()
}
func (wm *UserV3WriteModel) NewCreated(
ctx context.Context,
schemaID string,
schemaRevision uint64,
data json.RawMessage,
email *Email,
phone *Phone,
emailCode func(context.Context) (*EncryptedCode, error),
phoneCode func(context.Context) (*EncryptedCode, string, error),
) (_ []eventstore.Command, codeEmail string, codePhone string, err error) {
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", "", err
}
if wm.Exists() {
return nil, "", "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.AlreadyExists")
}
events := []eventstore.Command{
schemauser.NewCreatedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
schemaID, schemaRevision, data,
),
}
if email != nil {
emailEvents, plainCodeEmail, err := wm.NewEmailCreate(ctx,
email,
emailCode,
)
if err != nil {
return nil, "", "", err
}
if plainCodeEmail != "" {
codeEmail = plainCodeEmail
}
events = append(events, emailEvents...)
}
if phone != nil {
phoneEvents, plainCodePhone, err := wm.NewPhoneCreate(ctx,
phone,
phoneCode,
)
if err != nil {
return nil, "", "", err
}
if plainCodePhone != "" {
codePhone = plainCodePhone
}
events = append(events, phoneEvents...)
}
return events, codeEmail, codePhone, nil
}
func (wm *UserV3WriteModel) getSchemaRoleForWrite(ctx context.Context, resourceOwner, userID string) (domain_schema.Role, error) {
if userID == authz.GetCtxData(ctx).UserID {
return domain_schema.RoleSelf, nil
}
if err := wm.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
return domain_schema.RoleUnspecified, err
}
return domain_schema.RoleOwner, nil
}
func (wm *UserV3WriteModel) validateData(ctx context.Context, data []byte, schemaWM *UserSchemaWriteModel) (string, uint64, error) {
// get role for permission check in schema through extension
role, err := wm.getSchemaRoleForWrite(ctx, wm.ResourceOwner, wm.AggregateID)
if err != nil {
return "", 0, err
}
schema, err := domain_schema.NewSchema(role, bytes.NewReader(schemaWM.Schema))
if err != nil {
return "", 0, err
}
// if data not changed but a new schema or revision should be used
if data == nil {
data = wm.Data
}
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return "", 0, zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid")
}
if err := schema.Validate(v); err != nil {
return "", 0, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")
}
return schemaWM.AggregateID, schemaWM.SchemaRevision, nil
}
func (wm *UserV3WriteModel) NewUpdate(
ctx context.Context,
schemaWM *UserSchemaWriteModel,
user *SchemaUser,
email *Email,
phone *Phone,
emailCode func(context.Context) (*EncryptedCode, error),
phoneCode func(context.Context) (*EncryptedCode, string, error),
) (_ []eventstore.Command, codeEmail string, codePhone string, err error) {
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", "", err
}
if !wm.Exists() {
return nil, "", "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.NotFound")
}
events := make([]eventstore.Command, 0)
if user != nil {
schemaID, schemaRevision, err := wm.validateData(ctx, user.Data, schemaWM)
if err != nil {
return nil, "", "", err
}
userEvents := wm.newUpdatedEvents(ctx,
schemaID,
schemaRevision,
user.Data,
)
events = append(events, userEvents...)
}
if email != nil {
emailEvents, plainCodeEmail, err := wm.NewEmailUpdate(ctx,
email,
emailCode,
)
if err != nil {
return nil, "", "", err
}
if plainCodeEmail != "" {
codeEmail = plainCodeEmail
}
events = append(events, emailEvents...)
}
if phone != nil {
phoneEvents, plainCodePhone, err := wm.NewPhoneCreate(ctx,
phone,
phoneCode,
)
if err != nil {
return nil, "", "", err
}
if plainCodePhone != "" {
codePhone = plainCodePhone
}
events = append(events, phoneEvents...)
}
return events, codeEmail, codePhone, nil
}
func (wm *UserV3WriteModel) newUpdatedEvents(
ctx context.Context,
schemaID string,
schemaRevision uint64,
data json.RawMessage,
) []eventstore.Command {
changes := make([]schemauser.Changes, 0)
if wm.SchemaID != schemaID {
changes = append(changes, schemauser.ChangeSchemaID(schemaID))
}
if wm.SchemaRevision != schemaRevision {
changes = append(changes, schemauser.ChangeSchemaRevision(schemaRevision))
}
if data != nil && !bytes.Equal(wm.Data, data) {
changes = append(changes, schemauser.ChangeData(data))
}
if len(changes) == 0 {
return nil
}
return []eventstore.Command{schemauser.NewUpdatedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel), changes)}
}
func (wm *UserV3WriteModel) NewDelete(
ctx context.Context,
) (_ []eventstore.Command, err error) {
if !wm.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound")
}
if err := wm.checkPermissionDelete(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, err
}
return []eventstore.Command{schemauser.NewDeletedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))}, nil
}
func UserV3AggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: wm.AggregateID,
Type: schemauser.AggregateType,
ResourceOwner: wm.ResourceOwner,
InstanceID: wm.InstanceID,
Version: schemauser.AggregateVersion,
}
}
func (wm *UserV3WriteModel) Exists() bool {
return wm.State != domain.UserStateDeleted && wm.State != domain.UserStateUnspecified
}
func (wm *UserV3WriteModel) checkPermissionWrite(
ctx context.Context,
resourceOwner string,
userID string,
) error {
if wm.writePermissionCheck {
return nil
}
if userID != "" && userID == authz.GetCtxData(ctx).UserID {
return nil
}
if err := wm.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
return err
}
wm.writePermissionCheck = true
return nil
}
func (wm *UserV3WriteModel) checkPermissionDelete(
ctx context.Context,
resourceOwner string,
userID string,
) error {
if userID != "" && userID == authz.GetCtxData(ctx).UserID {
return nil
}
return wm.checkPermission(ctx, domain.PermissionUserDelete, resourceOwner, userID)
}
func (wm *UserV3WriteModel) NewEmailCreate(
ctx context.Context,
email *Email,
code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, plainCode string, err error) {
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", err
}
if email == nil || wm.Email == string(email.Address) {
return nil, "", nil
}
events := []eventstore.Command{
schemauser.NewEmailUpdatedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
email.Address,
),
}
if email.Verified {
events = append(events, wm.newEmailVerifiedEvent(ctx))
} else {
codeEvent, code, err := wm.newEmailCodeAddedEvent(ctx, code, email.URLTemplate, email.ReturnCode)
if err != nil {
return nil, "", err
}
events = append(events, codeEvent)
if code != "" {
plainCode = code
}
}
return events, plainCode, nil
}
func (wm *UserV3WriteModel) NewEmailUpdate(
ctx context.Context,
email *Email,
code func(context.Context) (*EncryptedCode, error),
) (_ []eventstore.Command, plainCode string, err error) {
if !wm.EmailWM {
return nil, "", nil
}
if !wm.Exists() {
return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-nJ0TQFuRmP", "Errors.User.NotFound")
}
return wm.NewEmailCreate(ctx, email, code)
}
func (wm *UserV3WriteModel) NewEmailVerify(
ctx context.Context,
verify func(creationDate time.Time, expiry time.Duration, cryptoCode *crypto.CryptoValue) error,
) ([]eventstore.Command, error) {
if !wm.EmailWM {
return nil, nil
}
if !wm.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-qbGyMPvjvj", "Errors.User.NotFound")
}
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, err
}
if wm.EmailCode == nil {
return nil, nil
}
if err := verify(wm.EmailCode.CreationDate, wm.EmailCode.Expiry, wm.EmailCode.Code); err != nil {
return nil, err
}
return []eventstore.Command{wm.newEmailVerifiedEvent(ctx)}, nil
}
func (wm *UserV3WriteModel) newEmailVerifiedEvent(
ctx context.Context,
) *schemauser.EmailVerifiedEvent {
return schemauser.NewEmailVerifiedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))
}
func (wm *UserV3WriteModel) NewResendEmailCode(
ctx context.Context,
code func(context.Context) (*EncryptedCode, error),
urlTemplate string,
isReturnCode bool,
) (_ []eventstore.Command, plainCode string, err error) {
if !wm.EmailWM {
return nil, "", nil
}
if !wm.Exists() {
return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-EajeF6ypOV", "Errors.User.NotFound")
}
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", err
}
if wm.EmailCode == nil {
return nil, "", zerrors.ThrowPreconditionFailed(err, "COMMAND-QRkNTBwF8q", "Errors.User.Code.Empty")
}
event, plainCode, err := wm.newEmailCodeAddedEvent(ctx, code, urlTemplate, isReturnCode)
if err != nil {
return nil, "", err
}
return []eventstore.Command{event}, plainCode, nil
}
func (wm *UserV3WriteModel) newEmailCodeAddedEvent(
ctx context.Context,
code func(context.Context) (*EncryptedCode, error),
urlTemplate string,
isReturnCode bool,
) (_ *schemauser.EmailCodeAddedEvent, plainCode string, err error) {
cryptoCode, err := code(ctx)
if err != nil {
return nil, "", err
}
if isReturnCode {
plainCode = cryptoCode.Plain
}
return schemauser.NewEmailCodeAddedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
cryptoCode.Crypted,
cryptoCode.Expiry,
urlTemplate,
isReturnCode,
), plainCode, nil
}
func (wm *UserV3WriteModel) NewPhoneCreate(
ctx context.Context,
phone *Phone,
code func(context.Context) (*EncryptedCode, string, error),
) (_ []eventstore.Command, plainCode string, err error) {
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", err
}
if phone == nil || wm.Phone == string(phone.Number) {
return nil, "", nil
}
events := []eventstore.Command{
schemauser.NewPhoneUpdatedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
phone.Number,
),
}
if phone.Verified {
events = append(events, wm.newPhoneVerifiedEvent(ctx))
} else {
codeEvent, code, err := wm.newPhoneCodeAddedEvent(ctx, code, phone.ReturnCode)
if err != nil {
return nil, "", err
}
events = append(events, codeEvent)
if code != "" {
plainCode = code
}
}
return events, plainCode, nil
}
func (wm *UserV3WriteModel) NewPhoneUpdate(
ctx context.Context,
phone *Phone,
code func(context.Context) (*EncryptedCode, string, error),
) (_ []eventstore.Command, plainCode string, err error) {
if !wm.PhoneWM {
return nil, "", nil
}
if !wm.Exists() {
return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-b33QAVgel6", "Errors.User.NotFound")
}
return wm.NewPhoneCreate(ctx, phone, code)
}
func (wm *UserV3WriteModel) NewPhoneVerify(
ctx context.Context,
verify func(creationDate time.Time, expiry time.Duration, cryptoCode *crypto.CryptoValue) error,
) ([]eventstore.Command, error) {
if !wm.PhoneWM {
return nil, nil
}
if !wm.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-bx2OLtgGNS", "Errors.User.NotFound")
}
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, err
}
if wm.PhoneCode == nil {
return nil, nil
}
if err := verify(wm.PhoneCode.CreationDate, wm.PhoneCode.Expiry, wm.PhoneCode.Code); err != nil {
return nil, err
}
return []eventstore.Command{wm.newPhoneVerifiedEvent(ctx)}, nil
}
func (wm *UserV3WriteModel) newPhoneVerifiedEvent(
ctx context.Context,
) *schemauser.PhoneVerifiedEvent {
return schemauser.NewPhoneVerifiedEvent(ctx, UserV3AggregateFromWriteModel(&wm.WriteModel))
}
func (wm *UserV3WriteModel) NewResendPhoneCode(
ctx context.Context,
code func(context.Context) (*EncryptedCode, string, error),
isReturnCode bool,
) (_ []eventstore.Command, plainCode string, err error) {
if !wm.PhoneWM {
return nil, "", nil
}
if !wm.Exists() {
return nil, "", zerrors.ThrowNotFound(nil, "COMMAND-z8Bu9vuL9s", "Errors.User.NotFound")
}
if err := wm.checkPermissionWrite(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, "", err
}
if wm.PhoneCode == nil {
return nil, "", zerrors.ThrowPreconditionFailed(err, "COMMAND-fEsHdqECzb", "Errors.User.Code.Empty")
}
event, plainCode, err := wm.newPhoneCodeAddedEvent(ctx, code, isReturnCode)
if err != nil {
return nil, "", err
}
return []eventstore.Command{event}, plainCode, nil
}
func (wm *UserV3WriteModel) newPhoneCodeAddedEvent(
ctx context.Context,
code func(context.Context) (*EncryptedCode, string, error),
isReturnCode bool,
) (_ *schemauser.PhoneCodeAddedEvent, plainCode string, err error) {
cryptoCode, generatorID, err := code(ctx)
if err != nil {
return nil, "", err
}
if isReturnCode {
plainCode = cryptoCode.Plain
}
return schemauser.NewPhoneCodeAddedEvent(ctx,
UserV3AggregateFromWriteModel(&wm.WriteModel),
cryptoCode.CryptedCode(),
cryptoCode.CodeExpiry(),
isReturnCode,
generatorID,
), plainCode, nil
}