feat(api): add user creation to user service (#5745)

* chore(proto): update versions

* change protoc plugin

* some cleanups

* define api for setting emails in new api

* implement user.SetEmail

* move SetEmail buisiness logic into command

* resuse newCryptoCode

* command: add ChangeEmail unit tests

Not complete, was not able to mock the generator.

* Revert "resuse newCryptoCode"

This reverts commit c89e90ae35.

* undo change to crypto code generators

* command: use a generator so we can test properly

* command: reorganise ChangeEmail

improve test coverage

* implement VerifyEmail

including unit tests

* add URL template tests

* begin user creation

* change protos

* implement metadata and move context

* merge commands

* proto: change context to object

* remove old auth option

* remove old auth option

* fix linting errors

run gci on modified files

* add permission checks and fix some errors

* comments

* comments

* update email requests

* rename proto requests

* cleanup and docs

* simplify

* simplify

* fix setup

* remove unused proto messages / fields

---------

Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
Livio Spring
2023-04-26 07:47:57 +02:00
committed by GitHub
parent 19f2f83b61
commit e4a4b7cfbe
24 changed files with 1175 additions and 226 deletions

View File

@@ -30,6 +30,7 @@ type Commands struct {
httpClient *http.Client
checkPermission permissionCheck
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
eventstore *eventstore.Eventstore
static static.Storage
@@ -109,6 +110,7 @@ func StartCommands(
checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf)
},
newEmailCode: newEmailCode,
}
instance_repo.RegisterEventMappers(repo.eventstore)

View File

@@ -10,24 +10,33 @@ import (
"github.com/zitadel/zitadel/internal/errors"
)
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, expiry time.Duration, err error) {
type CryptoCodeWithExpiry struct {
Crypted *crypto.CryptoValue
Plain string
Expiry time.Duration
}
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) {
config, err := secretGeneratorConfig(ctx, filter, typ)
if err != nil {
return nil, -1, err
return nil, err
}
code := new(CryptoCodeWithExpiry)
switch a := alg.(type) {
case crypto.HashAlgorithm:
value, _, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
case crypto.EncryptionAlgorithm:
value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
default:
return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
return nil, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
}
if err != nil {
return nil, -1, err
return nil, err
}
return value, config.Expiry, nil
code.Expiry = config.Expiry
return code, nil
}
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {

View File

@@ -2,7 +2,6 @@ package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
@@ -12,12 +11,18 @@ import (
type Email struct {
Address domain.EmailAddress
Verified bool
// ReturnCode is used if the Verified field is false
ReturnCode bool
// URLTemplate can be used to specify a custom link to be sent in the mail verification
URLTemplate string
}
func (e *Email) Validate() error {
return e.Address.Validate()
}
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
}

View File

@@ -333,8 +333,9 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize))
}
} else if setup.Org.Human != nil {
setup.Org.Human.ID = userAgg.ID
validations = append(validations,
AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption),
c.AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption, true),
)
}

View File

@@ -51,7 +51,7 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user
var pat *PersonalAccessToken
var machineKey *MachineKey
if o.Human != nil {
validations = append(validations, AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption))
validations = append(validations, c.AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption, true))
} else if o.Machine != nil {
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
if o.Machine.Pat != nil {

View File

@@ -2,7 +2,6 @@ package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
@@ -14,6 +13,6 @@ type Phone struct {
Verified bool
}
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
}

View File

@@ -439,7 +439,7 @@ func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id
return exists, nil
}
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
}

View File

@@ -27,6 +27,8 @@ func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) (
}
type AddHuman struct {
// ID is optional
ID string
// Username is required
Username string
// FirstName is required
@@ -43,63 +45,99 @@ type AddHuman struct {
PreferredLanguage language.Tag
// Gender is required
Gender domain.Gender
//Phone represents an international phone number
// Phone represents an international phone number
Phone Phone
//Password is optional
// Password is optional
Password string
//BcryptedPassword is optional
// BcryptedPassword is optional
BcryptedPassword string
//PasswordChangeRequired is used if the `Password`-field is set
// PasswordChangeRequired is used if the `Password`-field is set
PasswordChangeRequired bool
Passwordless bool
ExternalIDP bool
Register bool
Metadata []*AddMetadataEntry
// Details are set after a successful execution of the command
Details *domain.ObjectDetails
// EmailCode is set by the command
EmailCode *string
}
func (c *Commands) AddHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) {
existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
func (h *AddHuman) Validate() (err error) {
if err := h.Email.Validate(); err != nil {
return err
}
if isUserStateExists(existingHuman.UserState) {
return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
if h.Username = strings.TrimSpace(h.Username); h.Username == "" {
return errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
}
return c.addHumanWithID(ctx, resourceOwner, userID, human)
if h.FirstName = strings.TrimSpace(h.FirstName); h.FirstName == "" {
return errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
}
if h.LastName = strings.TrimSpace(h.LastName); h.LastName == "" {
return errors.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
}
}
return nil
}
func (c *Commands) addHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) {
agg := user.NewAggregate(userID, resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddHumanCommand(agg, human, c.userPasswordAlg, c.userEncryption))
type AddMetadataEntry struct {
Key string
Value []byte
}
func (m *AddMetadataEntry) Valid() error {
if m.Key = strings.TrimSpace(m.Key); m.Key == "" {
return errors.ThrowInvalidArgument(nil, "USER-Drght", "Errors.User.Metadata.KeyEmpty")
}
if len(m.Value) == 0 {
return errors.ThrowInvalidArgument(nil, "USER-Dbgth", "Errors.User.Metadata.ValueEmpty")
}
return nil
}
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool) (err error) {
if resourceOwner == "" {
return errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
}
agg := user.NewAggregate(human.ID, resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
c.AddHumanCommand(
agg,
human,
c.userPasswordAlg,
c.userEncryption,
allowInitMail,
))
if err != nil {
return nil, err
return err
}
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
return err
}
human.Details = &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
}
return &domain.HumanDetails{
ID: userID,
ObjectDetails: domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
},
}, nil
}
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) {
if resourceOwner == "" {
return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
}
userID, err := c.idGenerator.Next()
if err != nil {
return nil, err
}
return c.addHumanWithID(ctx, resourceOwner, userID, human)
return nil
}
type humanCreationCommand interface {
@@ -108,30 +146,17 @@ type humanCreationCommand interface {
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
}
func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm) preparation.Validation {
func (c *Commands) AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if err := human.Email.Validate(); err != nil {
if err := human.Validate(); err != nil {
return nil, err
}
if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
}
if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
}
if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
}
human.ensureDisplayName()
if human.Phone.Number != "" {
if human.Phone.Number, err = human.Phone.Number.Normalize(); err != nil {
return nil, err
}
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if err := c.addHumanCommandCheckID(ctx, filter, a, human); err != nil {
return nil, err
}
domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ResourceOwner)
if err != nil {
return nil, err
@@ -176,55 +201,30 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
createCmd.AddPhoneData(human.Phone.Number)
}
if human.Password != "" {
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
return nil, err
}
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
if err != nil {
return nil, err
}
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
}
if human.BcryptedPassword != "" {
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
if err := addHumanCommandPassword(ctx, filter, createCmd, human, passwordAlg); err != nil {
return nil, err
}
cmds := make([]eventstore.Command, 0, 3)
cmds = append(cmds, createCmd)
if human.Email.Verified {
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
}
//add init code if
// email not verified or
// user not registered and password set
if human.shouldAddInitCode() {
value, expiry, err := newUserInitCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
} else {
if !human.Email.Verified {
value, expiry, err := newEmailCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
}
cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, a, human, codeAlg, allowInitMail)
if err != nil {
return nil, err
}
if human.Phone.Verified {
cmds = append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate))
} else if human.Phone.Number != "" {
value, expiry, err := newPhoneCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
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,
))
}
return cmds, nil
@@ -232,6 +232,87 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
}
}
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 := newUserInitCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry)), 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)), nil
}
return cmds, 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, err := newPhoneCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
return append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry)), nil
}
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, a *user.Aggregate, human *AddHuman) (err error) {
if human.ID != "" {
existingHuman, err := humanWriteModelByID(ctx, filter, human.ID, a.ResourceOwner)
if err != nil {
return err
}
if isUserStateExists(existingHuman.UserState) {
return errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
}
return nil
}
human.ID, err = c.idGenerator.Next()
if err != nil {
return err
}
a.ID = human.ID
return nil
}
func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, passwordAlg crypto.HashAlgorithm) (err error) {
if human.Password != "" {
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
return err
}
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
if err != nil {
return err
}
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
return nil
}
if human.BcryptedPassword != "" {
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
}
return nil
}
func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error {
if mustBeDomain {
return nil
@@ -651,3 +732,17 @@ func (c *Commands) getHumanWriteModelByID(ctx context.Context, userID, resourceo
}
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
}

View File

@@ -2,17 +2,19 @@ package command
import (
"context"
"errors"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -29,16 +31,20 @@ func TestCommandSide_AddHuman(t *testing.T) {
idGenerator id.Generator
userPasswordAlg crypto.HashAlgorithm
codeAlg crypto.EncryptionAlgorithm
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
}
type args struct {
ctx context.Context
orgID string
human *AddHuman
secretGenerator crypto.Generator
allowInitMail bool
}
type res struct {
want *domain.HumanDetails
err func(error) bool
want *domain.ObjectDetails
wantID string
wantEmailCode string
err func(error) bool
}
userAgg := user.NewAggregate("user1", "org1")
@@ -68,9 +74,67 @@ func TestCommandSide_AddHuman(t *testing.T) {
Address: "email@test.ch",
},
},
allowInitMail: true,
},
res: res{
err: errors.IsErrorInvalidArgument,
err: func(err error) bool {
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal"))
},
},
},
{
name: "user invalid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
FirstName: "firstname",
},
allowInitMail: true,
},
res: res{
err: func(err error) bool {
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"))
},
},
},
{
name: "with id, already exists, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
newAddHumanEvent("password", true, ""),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
ID: "user1",
Username: "username",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@test.ch",
},
PreferredLanguage: language.English,
},
allowInitMail: true,
},
res: res{
err: func(err error) bool {
return errors.Is(err, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting"))
},
},
},
{
@@ -95,9 +159,12 @@ func TestCommandSide_AddHuman(t *testing.T) {
},
PreferredLanguage: language.English,
},
allowInitMail: true,
},
res: res{
err: errors.IsInternal,
err: func(err error) bool {
return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal"))
},
},
},
{
@@ -134,30 +201,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
},
PreferredLanguage: language.English,
},
allowInitMail: true,
},
res: res{
err: errors.IsInternal,
},
},
{
name: "user invalid, invalid argument error",
fields: fields{
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
FirstName: "firstname",
err: func(err error) bool {
return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal"))
},
},
res: res{
err: errors.IsErrorInvalidArgument,
},
},
{
name: "add human (with initial code), ok",
@@ -237,16 +287,15 @@ func TestCommandSide_AddHuman(t *testing.T) {
PreferredLanguage: language.English,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.HumanDetails{
ID: "user1",
ObjectDetails: domain.ObjectDetails{
Sequence: 0,
EventDate: time.Time{},
ResourceOwner: "org1",
},
want: &domain.ObjectDetails{
Sequence: 0,
EventDate: time.Time{},
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
@@ -330,14 +379,172 @@ func TestCommandSide_AddHuman(t *testing.T) {
PreferredLanguage: language.English,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.HumanDetails{
ID: "user1",
ObjectDetails: domain.ObjectDetails{
ResourceOwner: "org1",
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
name: "add human (with password and email code custom template), ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
true,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newAddHumanEvent("password", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailCodeAddedEventV2(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("emailCode"),
},
1*time.Hour,
"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}",
false,
),
),
},
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
newEmailCode: mockEmailCode("emailCode", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
Password: "password",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@test.ch",
URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}",
},
PreferredLanguage: language.English,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: false,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
name: "add human (with password and return email code), ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
true,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newAddHumanEvent("password", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailCodeAddedEventV2(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("emailCode"),
},
1*time.Hour,
"",
true,
),
),
},
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
newEmailCode: mockEmailCode("emailCode", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
Password: "password",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@test.ch",
ReturnCode: true,
},
PreferredLanguage: language.English,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: false,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
wantEmailCode: "emailCode",
},
},
{
@@ -400,14 +607,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
PasswordChangeRequired: true,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.HumanDetails{
ID: "user1",
ObjectDetails: domain.ObjectDetails{
ResourceOwner: "org1",
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
@@ -470,14 +676,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
PasswordChangeRequired: true,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.HumanDetails{
ID: "user1",
ObjectDetails: domain.ObjectDetails{
ResourceOwner: "org1",
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
@@ -540,14 +745,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
PasswordChangeRequired: true,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.HumanDetails{
ID: "user1",
ObjectDetails: domain.ObjectDetails{
ResourceOwner: "org1",
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
@@ -594,9 +798,12 @@ func TestCommandSide_AddHuman(t *testing.T) {
PasswordChangeRequired: true,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
err: errors.IsErrorInvalidArgument,
err: func(err error) bool {
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername"))
},
},
},
{
@@ -687,15 +894,14 @@ func TestCommandSide_AddHuman(t *testing.T) {
PasswordChangeRequired: true,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.HumanDetails{
ID: "user1",
ObjectDetails: domain.ObjectDetails{
ResourceOwner: "org1",
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
@@ -787,14 +993,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
PreferredLanguage: language.English,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.HumanDetails{
ID: "user1",
ObjectDetails: domain.ObjectDetails{
ResourceOwner: "org1",
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
@@ -875,14 +1080,104 @@ func TestCommandSide_AddHuman(t *testing.T) {
PreferredLanguage: language.English,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.HumanDetails{
ID: "user1",
ObjectDetails: domain.ObjectDetails{
ResourceOwner: "org1",
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
name: "add human with metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&userAgg.Aggregate,
true,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(
context.Background(),
&instanceAgg.Aggregate,
domain.SecretGeneratorTypeInitCode,
0,
1*time.Hour,
true,
true,
true,
true,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newAddHumanEvent("", false, ""),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(
context.Background(),
&userAgg.Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte(""),
},
1*time.Hour,
),
),
eventFromEventPusher(
user.NewMetadataSetEvent(
context.Background(),
&userAgg.Aggregate,
"testKey",
[]byte("testValue"),
),
),
},
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@test.ch",
},
PreferredLanguage: language.English,
Metadata: []*AddMetadataEntry{
{
Key: "testKey",
Value: []byte("testValue"),
},
},
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
}
@@ -893,8 +1188,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
userPasswordAlg: tt.fields.userPasswordAlg,
userEncryption: tt.fields.codeAlg,
idGenerator: tt.fields.idGenerator,
newEmailCode: tt.fields.newEmailCode,
}
got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human)
err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail)
if tt.res.err == nil {
if !assert.NoError(t, err) {
t.FailNow()
@@ -904,7 +1200,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assert.Equal(t, tt.res.want, tt.args.human.Details)
assert.Equal(t, tt.res.wantID, tt.args.human.ID)
assert.Equal(t, tt.res.wantEmailCode, gu.Value(tt.args.human.EmailCode))
}
})
}
@@ -958,7 +1256,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
res: res{
err: errors.IsErrorInvalidArgument,
err: caos_errs.IsErrorInvalidArgument,
},
},
{
@@ -985,7 +1283,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
res: res{
err: errors.IsPreconditionFailed,
err: caos_errs.IsPreconditionFailed,
},
},
{
@@ -1022,7 +1320,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
res: res{
err: errors.IsPreconditionFailed,
err: caos_errs.IsPreconditionFailed,
},
},
{
@@ -1065,7 +1363,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
res: res{
err: errors.IsErrorInvalidArgument,
err: caos_errs.IsErrorInvalidArgument,
},
},
{
@@ -1869,7 +2167,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
},
},
res: res{
err: errors.IsErrorInvalidArgument,
err: caos_errs.IsErrorInvalidArgument,
},
},
{
@@ -1899,7 +2197,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
},
},
res: res{
err: errors.IsPreconditionFailed,
err: caos_errs.IsPreconditionFailed,
},
},
{
@@ -1939,7 +2237,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
},
},
res: res{
err: errors.IsPreconditionFailed,
err: caos_errs.IsPreconditionFailed,
},
},
{
@@ -1987,7 +2285,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
},
},
res: res{
err: errors.IsPreconditionFailed,
err: caos_errs.IsPreconditionFailed,
},
},
{
@@ -2056,7 +2354,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
},
},
res: res{
err: errors.IsPreconditionFailed,
err: caos_errs.IsPreconditionFailed,
},
},
{
@@ -2125,7 +2423,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
},
},
res: res{
err: errors.IsErrorInvalidArgument,
err: caos_errs.IsErrorInvalidArgument,
},
},
{
@@ -2211,7 +2509,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
},
},
res: res{
err: errors.IsErrorInvalidArgument,
err: caos_errs.IsErrorInvalidArgument,
},
},
{
@@ -3147,7 +3445,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) {
userID: "",
},
res: res{
err: errors.IsErrorInvalidArgument,
err: caos_errs.IsErrorInvalidArgument,
},
},
{
@@ -3164,7 +3462,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) {
userID: "user1",
},
res: res{
err: errors.IsNotFound,
err: caos_errs.IsNotFound,
},
},
{
@@ -3261,7 +3559,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
userIDs: []string{"user1"},
},
res: res{
err: errors.IsErrorInvalidArgument,
err: caos_errs.IsErrorInvalidArgument,
},
},
{
@@ -3277,7 +3575,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
userIDs: []string{},
},
res: res{
err: errors.IsErrorInvalidArgument,
err: caos_errs.IsErrorInvalidArgument,
},
},
{
@@ -3479,18 +3777,23 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone
}
func TestAddHumanCommand(t *testing.T) {
type fields struct {
idGenerator id.Generator
}
type args struct {
a *user.Aggregate
human *AddHuman
passwordAlg crypto.HashAlgorithm
filter preparation.FilterToQueryReducer
codeAlg crypto.EncryptionAlgorithm
a *user.Aggregate
human *AddHuman
passwordAlg crypto.HashAlgorithm
filter preparation.FilterToQueryReducer
codeAlg crypto.EncryptionAlgorithm
allowInitMail bool
}
agg := user.NewAggregate("id", "ro")
tests := []struct {
name string
args args
want Want
name string
fields fields
args args
want Want
}{
{
name: "invalid email",
@@ -3503,7 +3806,7 @@ func TestAddHumanCommand(t *testing.T) {
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
},
},
{
@@ -3519,7 +3822,7 @@ func TestAddHumanCommand(t *testing.T) {
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
},
},
{
@@ -3534,11 +3837,14 @@ func TestAddHumanCommand(t *testing.T) {
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
},
},
{
name: "invalid password",
fields: fields{
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
},
args: args{
a: agg,
human: &AddHuman{
@@ -3578,11 +3884,14 @@ func TestAddHumanCommand(t *testing.T) {
Filter(),
},
want: Want{
CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
CreateErr: caos_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
},
},
{
name: "correct",
fields: fields{
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
},
args: args{
a: agg,
human: &AddHuman{
@@ -3654,7 +3963,25 @@ func TestAddHumanCommand(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, context.Background(), AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg, tt.args.codeAlg), tt.args.filter, tt.want)
c := &Commands{
idGenerator: tt.fields.idGenerator,
}
AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want)
})
}
}
func mockEmailCode(code string, exp time.Duration) func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
return &CryptoCodeWithExpiry{
Crypted: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte(code),
},
Plain: code,
Expiry: exp,
}, nil
}
}