diff --git a/internal/admin/repository/eventsourcing/eventstore/org.go b/internal/admin/repository/eventsourcing/eventstore/org.go index b2f639e32..ade3b8a32 100644 --- a/internal/admin/repository/eventsourcing/eventstore/org.go +++ b/internal/admin/repository/eventsourcing/eventstore/org.go @@ -10,13 +10,19 @@ import ( org_model "github.com/caos/zitadel/internal/org/model" org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" org_view "github.com/caos/zitadel/internal/org/repository/view" + policy_es "github.com/caos/zitadel/internal/policy/repository/eventsourcing" usr_es "github.com/caos/zitadel/internal/user/repository/eventsourcing" ) +const ( + DEFAULT_POLICY = "0" +) + type OrgRepo struct { - Eventstore eventstore.Eventstore - OrgEventstore *org_es.OrgEventstore - UserEventstore *usr_es.UserEventstore + Eventstore eventstore.Eventstore + OrgEventstore *org_es.OrgEventstore + UserEventstore *usr_es.UserEventstore + PolicyEventstore *policy_es.PolicyEventstore View *admin_view.View @@ -24,12 +30,16 @@ type OrgRepo struct { } func (repo *OrgRepo) SetUpOrg(ctx context.Context, setUp *admin_model.SetupOrg) (*admin_model.SetupOrg, error) { + policy, err := repo.PolicyEventstore.GetPasswordComplexityPolicy(ctx, DEFAULT_POLICY) + if err != nil { + return nil, err + } org, aggregates, err := repo.OrgEventstore.PrepareCreateOrg(ctx, setUp.Org) if err != nil { return nil, err } - user, userAggregates, err := repo.UserEventstore.PrepareCreateUser(ctx, setUp.User, org.AggregateID) + user, userAggregates, err := repo.UserEventstore.PrepareCreateUser(ctx, setUp.User, policy, org.AggregateID) if err != nil { return nil, err } diff --git a/internal/admin/repository/eventsourcing/eventstore/user.go b/internal/admin/repository/eventsourcing/eventstore/user.go index c1cb5e448..f225f1fc0 100644 --- a/internal/admin/repository/eventsourcing/eventstore/user.go +++ b/internal/admin/repository/eventsourcing/eventstore/user.go @@ -2,12 +2,15 @@ package eventstore import ( "context" + "github.com/caos/zitadel/internal/api/auth" + policy_event "github.com/caos/zitadel/internal/policy/repository/eventsourcing" usr_model "github.com/caos/zitadel/internal/user/model" usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" ) type UserRepo struct { - UserEvents *usr_event.UserEventstore + UserEvents *usr_event.UserEventstore + PolicyEvents *policy_event.PolicyEventstore } func (repo *UserRepo) UserByID(ctx context.Context, id string) (project *usr_model.User, err error) { @@ -15,9 +18,21 @@ func (repo *UserRepo) UserByID(ctx context.Context, id string) (project *usr_mod } func (repo *UserRepo) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) { - return repo.UserEvents.CreateUser(ctx, user) + policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, auth.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return repo.UserEvents.CreateUser(ctx, user, policy) } func (repo *UserRepo) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) { - return repo.UserEvents.RegisterUser(ctx, user, resourceOwner) + policyResourceOwner := auth.GetCtxData(ctx).OrgID + if resourceOwner != "" { + policyResourceOwner = resourceOwner + } + policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, policyResourceOwner) + if err != nil { + return nil, err + } + return repo.UserEvents.RegisterUser(ctx, user, policy, resourceOwner) } diff --git a/internal/admin/repository/eventsourcing/repository.go b/internal/admin/repository/eventsourcing/repository.go index 18c75bc09..9defdfba9 100644 --- a/internal/admin/repository/eventsourcing/repository.go +++ b/internal/admin/repository/eventsourcing/repository.go @@ -2,6 +2,7 @@ package eventsourcing import ( "context" + es_policy "github.com/caos/zitadel/internal/policy/repository/eventsourcing" "github.com/caos/logging" "github.com/caos/zitadel/internal/admin/repository/eventsourcing/eventstore" @@ -61,7 +62,13 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults) ( if err != nil { return nil, err } - + policy, err := es_policy.StartPolicy(es_policy.PolicyConfig{ + Eventstore: es, + Cache: conf.Eventstore.Cache, + }, systemDefaults) + if err != nil { + return nil, err + } sqlClient, err := conf.View.Start() if err != nil { return nil, err @@ -71,7 +78,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults) ( return nil, err } - eventstoreRepos := setup.EventstoreRepos{OrgEvents: org, UserEvents: user, ProjectEvents: project, IamEvents: iam} + eventstoreRepos := setup.EventstoreRepos{OrgEvents: org, UserEvents: user, ProjectEvents: project, IamEvents: iam, PolicyEvents: policy} err = setup.StartSetup(systemDefaults, eventstoreRepos).Execute(ctx) logging.Log("SERVE-k280HZ").OnError(err).Panic("failed to execute setup") diff --git a/internal/admin/repository/eventsourcing/setup/setup.go b/internal/admin/repository/eventsourcing/setup/setup.go index 8aa5615ec..631083c12 100644 --- a/internal/admin/repository/eventsourcing/setup/setup.go +++ b/internal/admin/repository/eventsourcing/setup/setup.go @@ -2,6 +2,8 @@ package setup import ( "context" + policy_model "github.com/caos/zitadel/internal/policy/model" + policy_event "github.com/caos/zitadel/internal/policy/repository/eventsourcing" "time" "github.com/caos/logging" @@ -31,13 +33,15 @@ type EventstoreRepos struct { OrgEvents *org_event.OrgEventstore UserEvents *usr_event.UserEventstore ProjectEvents *proj_event.ProjectEventstore + PolicyEvents *policy_event.PolicyEventstore } type initializer struct { *Setup - createdUsers map[string]*usr_model.User - createdOrgs map[string]*org_model.Org - createdProjects map[string]*proj_model.Project + createdUsers map[string]*usr_model.User + createdOrgs map[string]*org_model.Org + createdProjects map[string]*proj_model.Project + pwComplexityPolicy *policy_model.PasswordComplexityPolicy } const ( @@ -55,6 +59,7 @@ const ( OIDCAuthMethodType_NONE = "NONE" OIDCAuthMethodType_BASIC = "BASIC" OIDCAuthMethodType_POST = "POST" + DEFAULT_POLICY = "0" ) func StartSetup(sd systemdefaults.SystemDefaults, repos EventstoreRepos) *Setup { @@ -92,6 +97,13 @@ func (s *Setup) Execute(ctx context.Context) error { createdProjects: make(map[string]*proj_model.Project), } + pwComplexityPolicy, err := s.repos.PolicyEvents.GetPasswordComplexityPolicy(ctx, DEFAULT_POLICY) + if err != nil { + logging.Log("SETUP-9osWF").WithError(err).Error("unable to read complexity policy") + return err + } + setUp.pwComplexityPolicy = pwComplexityPolicy + err = setUp.orgs(ctx, s.setUpConfig.Orgs) if err != nil { logging.Log("SETUP-p4oWq").WithError(err).Error("unable to set up orgs") @@ -264,7 +276,7 @@ func (setUp *initializer) user(ctx context.Context, user types.User) (*usr_model SecretString: user.Password, }, } - return setUp.repos.UserEvents.CreateUser(ctx, createUser) + return setUp.repos.UserEvents.CreateUser(ctx, createUser, setUp.pwComplexityPolicy) } func (setUp *initializer) orgOwners(ctx context.Context, org *org_model.Org, owners []string) error { diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index 776d09621..7ffa70731 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -7,13 +7,15 @@ import ( "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" "github.com/caos/zitadel/internal/errors" es_models "github.com/caos/zitadel/internal/eventstore/models" + policy_event "github.com/caos/zitadel/internal/policy/repository/eventsourcing" "github.com/caos/zitadel/internal/user/model" user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" ) type UserRepo struct { - UserEvents *user_event.UserEventstore - View *view.View + UserEvents *user_event.UserEventstore + PolicyEvents *policy_event.PolicyEventstore + View *view.View } func (repo *UserRepo) Health(ctx context.Context) error { @@ -21,7 +23,15 @@ func (repo *UserRepo) Health(ctx context.Context) error { } func (repo *UserRepo) Register(ctx context.Context, user *model.User, resourceOwner string) (*model.User, error) { - return repo.UserEvents.RegisterUser(ctx, user, resourceOwner) + policyResourceOwner := auth.GetCtxData(ctx).OrgID + if resourceOwner != "" { + policyResourceOwner = resourceOwner + } + policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, policyResourceOwner) + if err != nil { + return nil, err + } + return repo.UserEvents.RegisterUser(ctx, user, policy, resourceOwner) } func (repo *UserRepo) MyProfile(ctx context.Context) (*model.Profile, error) { @@ -85,7 +95,11 @@ func (repo *UserRepo) ChangeMyAddress(ctx context.Context, address *model.Addres } func (repo *UserRepo) ChangeMyPassword(ctx context.Context, old, new string) error { - _, err := repo.UserEvents.ChangePassword(ctx, auth.GetCtxData(ctx).UserID, old, new) + policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, auth.GetCtxData(ctx).OrgID) + if err != nil { + return err + } + _, err = repo.UserEvents.ChangePassword(ctx, policy, auth.GetCtxData(ctx).UserID, old, new) return err } @@ -114,7 +128,11 @@ func (repo *UserRepo) RequestPasswordReset(ctx context.Context, username string) } func (repo *UserRepo) SetPassword(ctx context.Context, userID, code, password string) error { - return repo.UserEvents.SetPassword(ctx, userID, code, password) + policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, auth.GetCtxData(ctx).OrgID) + if err != nil { + return err + } + return repo.UserEvents.SetPassword(ctx, policy, userID, code, password) } func (repo *UserRepo) SignOut(ctx context.Context, agentID, userID string) error { diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 408104f8f..c988f492d 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -13,6 +13,7 @@ import ( es_int "github.com/caos/zitadel/internal/eventstore" es_spol "github.com/caos/zitadel/internal/eventstore/spooler" "github.com/caos/zitadel/internal/id" + es_policy "github.com/caos/zitadel/internal/policy/repository/eventsourcing" es_user "github.com/caos/zitadel/internal/user/repository/eventsourcing" ) @@ -44,7 +45,16 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) if err != nil { return nil, err } - + policy, err := es_policy.StartPolicy( + es_policy.PolicyConfig{ + Eventstore: es, + Cache: conf.Eventstore.Cache, + }, + systemDefaults, + ) + if err != nil { + return nil, err + } user, err := es_user.StartUser( es_user.UserConfig{ Eventstore: es, @@ -66,8 +76,9 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) return &EsRepository{ spool, eventstore.UserRepo{ - UserEvents: user, - View: view, + UserEvents: user, + PolicyEvents: policy, + View: view, }, eventstore.AuthRequestRepo{ UserEvents: user, diff --git a/internal/management/repository/eventsourcing/eventstore/user.go b/internal/management/repository/eventsourcing/eventstore/user.go index 2c908e7d6..754c17136 100644 --- a/internal/management/repository/eventsourcing/eventstore/user.go +++ b/internal/management/repository/eventsourcing/eventstore/user.go @@ -2,16 +2,19 @@ package eventstore import ( "context" + "github.com/caos/zitadel/internal/api/auth" "github.com/caos/zitadel/internal/management/repository/eventsourcing/view" + policy_event "github.com/caos/zitadel/internal/policy/repository/eventsourcing" usr_model "github.com/caos/zitadel/internal/user/model" usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" "github.com/caos/zitadel/internal/user/repository/view/model" ) type UserRepo struct { - SearchLimit uint64 - UserEvents *usr_event.UserEventstore - View *view.View + SearchLimit uint64 + UserEvents *usr_event.UserEventstore + PolicyEvents *policy_event.PolicyEventstore + View *view.View } func (repo *UserRepo) UserByID(ctx context.Context, id string) (project *usr_model.User, err error) { @@ -19,11 +22,23 @@ func (repo *UserRepo) UserByID(ctx context.Context, id string) (project *usr_mod } func (repo *UserRepo) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) { - return repo.UserEvents.CreateUser(ctx, user) + policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, auth.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return repo.UserEvents.CreateUser(ctx, user, policy) } func (repo *UserRepo) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) { - return repo.UserEvents.RegisterUser(ctx, user, resourceOwner) + policyResourceOwner := auth.GetCtxData(ctx).OrgID + if resourceOwner != "" { + policyResourceOwner = resourceOwner + } + policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, policyResourceOwner) + if err != nil { + return nil, err + } + return repo.UserEvents.RegisterUser(ctx, user, policy, resourceOwner) } func (repo *UserRepo) DeactivateUser(ctx context.Context, id string) (*usr_model.User, error) { @@ -73,7 +88,11 @@ func (repo *UserRepo) UserMfas(ctx context.Context, userID string) ([]*usr_model } func (repo *UserRepo) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) { - return repo.UserEvents.SetOneTimePassword(ctx, password) + policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, auth.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return repo.UserEvents.SetOneTimePassword(ctx, policy, password) } func (repo *UserRepo) RequestSetPassword(ctx context.Context, id string, notifyType usr_model.NotificationType) error { diff --git a/internal/management/repository/eventsourcing/repository.go b/internal/management/repository/eventsourcing/repository.go index ca793768a..add525ea8 100644 --- a/internal/management/repository/eventsourcing/repository.go +++ b/internal/management/repository/eventsourcing/repository.go @@ -86,7 +86,7 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string) (*EsRe spooler: spool, OrgRepository: eventstore.OrgRepository{conf.SearchLimit, org, view, roles}, ProjectRepo: eventstore.ProjectRepo{conf.SearchLimit, project, view, roles}, - UserRepo: eventstore.UserRepo{conf.SearchLimit, user, view}, + UserRepo: eventstore.UserRepo{conf.SearchLimit, user, policy, view}, UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, usergrant, view}, PolicyRepo: eventstore.PolicyRepo{policy}, }, nil diff --git a/internal/policy/model/policy_age.go b/internal/policy/model/policy_age.go index e37cdf38e..003ab681a 100644 --- a/internal/policy/model/policy_age.go +++ b/internal/policy/model/policy_age.go @@ -1,6 +1,8 @@ package model -import "github.com/caos/zitadel/internal/eventstore/models" +import ( + "github.com/caos/zitadel/internal/eventstore/models" +) type PasswordAgePolicy struct { models.ObjectRoot diff --git a/internal/policy/model/policy_complecity_test.go b/internal/policy/model/policy_complecity_test.go new file mode 100644 index 000000000..4efea96d6 --- /dev/null +++ b/internal/policy/model/policy_complecity_test.go @@ -0,0 +1,197 @@ +package model + +import ( + "testing" +) + +func TestCheckPasswordComplexityPolicy(t *testing.T) { + type args struct { + policy *PasswordComplexityPolicy + password string + } + tests := []struct { + name string + args args + hasError bool + }{ + { + name: "has minlength ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: false, + HasUppercase: false, + HasSymbol: false, + HasNumber: false, + MinLength: 10, + }, + password: "password12", + }, + hasError: false, + }, + { + name: "has minlength not ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: false, + HasUppercase: false, + HasSymbol: false, + HasNumber: false, + MinLength: 10, + }, + password: "password", + }, + hasError: true, + }, + { + name: "has lowercase ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: true, + HasUppercase: false, + HasSymbol: false, + HasNumber: false, + MinLength: 0, + }, + password: "password", + }, + hasError: false, + }, + { + name: "has lowercase not ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: true, + HasUppercase: false, + HasSymbol: false, + HasNumber: false, + MinLength: 0, + }, + password: "PASSWORD", + }, + hasError: true, + }, + { + name: "has uppercase ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: false, + HasUppercase: true, + HasSymbol: false, + HasNumber: false, + MinLength: 0, + }, + password: "PASSWORD", + }, + hasError: false, + }, + { + name: "has uppercase not ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: false, + HasUppercase: true, + HasSymbol: false, + HasNumber: false, + MinLength: 0, + }, + password: "password", + }, + hasError: true, + }, + { + name: "has symbol ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: false, + HasUppercase: false, + HasSymbol: true, + HasNumber: false, + MinLength: 0, + }, + password: "!G$", + }, + hasError: false, + }, + { + name: "has symbol not ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: false, + HasUppercase: false, + HasSymbol: true, + HasNumber: false, + MinLength: 0, + }, + password: "PASSWORD", + }, + hasError: true, + }, + { + name: "has number ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: false, + HasUppercase: false, + HasSymbol: false, + HasNumber: true, + MinLength: 0, + }, + password: "123456", + }, + hasError: false, + }, + { + name: "has number not ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: false, + HasUppercase: false, + HasSymbol: false, + HasNumber: true, + MinLength: 0, + }, + password: "PASSWORD", + }, + hasError: true, + }, + { + name: "has everything ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: true, + HasUppercase: true, + HasSymbol: true, + HasNumber: true, + MinLength: 10, + }, + password: "Password1!", + }, + hasError: false, + }, + { + name: "has everything not ok", + args: args{ + policy: &PasswordComplexityPolicy{ + HasLowercase: true, + HasUppercase: true, + HasSymbol: true, + HasNumber: true, + MinLength: 10, + }, + password: "password", + }, + hasError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.policy.Check(tt.args.password) + if !tt.hasError && err != nil { + t.Errorf("should not get err: %v", err) + } + if tt.hasError && err == nil { + t.Errorf("should have error: %v", err) + } + }) + } +} diff --git a/internal/policy/model/policy_complexity.go b/internal/policy/model/policy_complexity.go index cae1ffe2b..ed7239af5 100644 --- a/internal/policy/model/policy_complexity.go +++ b/internal/policy/model/policy_complexity.go @@ -1,6 +1,17 @@ package model -import "github.com/caos/zitadel/internal/eventstore/models" +import ( + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/models" + "regexp" +) + +var ( + hasStringLowerCase = regexp.MustCompile(`[a-z]`).MatchString + hasStringUpperCase = regexp.MustCompile(`[A-Z]`).MatchString + hasNumber = regexp.MustCompile(`[0-9]`).MatchString + hasSymbol = regexp.MustCompile(`[^A-Za-z0-9]`).MatchString +) type PasswordComplexityPolicy struct { models.ObjectRoot @@ -17,3 +28,26 @@ type PasswordComplexityPolicy struct { func (p *PasswordComplexityPolicy) IsValid() bool { return p.Description != "" } + +func (p *PasswordComplexityPolicy) Check(password string) error { + if p.MinLength != 0 && uint64(len(password)) < p.MinLength { + return caos_errs.ThrowInvalidArgumentf(nil, "MODEL-HuJf6", "Passwordpolicy doesn't match: Minlength %v", p.MinLength) + } + + if p.HasLowercase && !hasStringLowerCase(password) { + return caos_errs.ThrowInvalidArgument(nil, "MODEL-co3Xw", "Passwordpolicy doesn't match: HasLowerCase") + } + + if p.HasUppercase && !hasStringUpperCase(password) { + return caos_errs.ThrowInvalidArgument(nil, "MODEL-VoaRj", "Passwordpolicy doesn't match: HasUpperCase") + } + + if p.HasNumber && !hasNumber(password) { + return caos_errs.ThrowInvalidArgument(nil, "MODEL-ZBv4H", "Passwordpolicy doesn't match: HasNumber") + } + + if p.HasSymbol && !hasSymbol(password) { + return caos_errs.ThrowInvalidArgument(nil, "MODEL-ZDLwA", "Passwordpolicy doesn't match: HasSymbol") + } + return nil +} diff --git a/internal/user/model/password.go b/internal/user/model/password.go index e1be0973a..d9d25963a 100644 --- a/internal/user/model/password.go +++ b/internal/user/model/password.go @@ -2,7 +2,9 @@ package model import ( "github.com/caos/zitadel/internal/crypto" + caos_errs "github.com/caos/zitadel/internal/errors" es_models "github.com/caos/zitadel/internal/eventstore/models" + policy_model "github.com/caos/zitadel/internal/policy/model" "time" ) @@ -33,10 +35,16 @@ func (p *Password) IsValid() bool { return p.AggregateID != "" && p.SecretString != "" } -func (p *Password) HashPasswordIfExisting(passwordAlg crypto.HashAlgorithm, onetime bool) error { +func (p *Password) HashPasswordIfExisting(policy *policy_model.PasswordComplexityPolicy, passwordAlg crypto.HashAlgorithm, onetime bool) error { if p.SecretString == "" { return nil } + if policy == nil { + return caos_errs.ThrowPreconditionFailed(nil, "MODEL-s8ifS", "Policy should not be nil") + } + if err := policy.Check(p.SecretString); err != nil { + return err + } secret, err := crypto.Hash([]byte(p.SecretString), passwordAlg) if err != nil { return err diff --git a/internal/user/model/user.go b/internal/user/model/user.go index e02c6b345..f6bc47ff2 100644 --- a/internal/user/model/user.go +++ b/internal/user/model/user.go @@ -1,6 +1,7 @@ package model import ( + policy_model "github.com/caos/zitadel/internal/policy/model" "time" "github.com/caos/zitadel/internal/crypto" @@ -85,9 +86,9 @@ func (u *User) IsOTPReady() bool { return u.OTP != nil && u.OTP.State == MFASTATE_READY } -func (u *User) HashPasswordIfExisting(passwordAlg crypto.HashAlgorithm, onetime bool) error { +func (u *User) HashPasswordIfExisting(policy *policy_model.PasswordComplexityPolicy, passwordAlg crypto.HashAlgorithm, onetime bool) error { if u.Password != nil { - return u.Password.HashPasswordIfExisting(passwordAlg, onetime) + return u.Password.HashPasswordIfExisting(policy, passwordAlg, onetime) } return nil } diff --git a/internal/user/repository/eventsourcing/eventstore.go b/internal/user/repository/eventsourcing/eventstore.go index 478109a5d..9047385fa 100644 --- a/internal/user/repository/eventsourcing/eventstore.go +++ b/internal/user/repository/eventsourcing/eventstore.go @@ -3,6 +3,8 @@ package eventsourcing import ( "context" "github.com/caos/zitadel/internal/id" + policy_model "github.com/caos/zitadel/internal/policy/model" + "github.com/pquerna/otp/totp" req_model "github.com/caos/zitadel/internal/auth_request/model" @@ -87,20 +89,19 @@ func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.U return model.UserToModel(user), nil } -func (es *UserEventstore) PrepareCreateUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*model.User, []*es_models.Aggregate, error) { +func (es *UserEventstore) PrepareCreateUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, resourceOwner string) (*model.User, []*es_models.Aggregate, error) { user.SetEmailAsUsername() if !user.IsValid() { return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "User is invalid") } - //TODO: Check Uniqueness id, err := es.idGenerator.Next() if err != nil { return nil, nil, err } user.AggregateID = id - err = user.HashPasswordIfExisting(es.PasswordAlg, true) + err = user.HashPasswordIfExisting(policy, es.PasswordAlg, true) if err != nil { return nil, nil, err } @@ -122,8 +123,8 @@ func (es *UserEventstore) PrepareCreateUser(ctx context.Context, user *usr_model return repoUser, createAggregates, err } -func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) { - repoUser, aggregates, err := es.PrepareCreateUser(ctx, user, "") +func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy) (*usr_model.User, error) { + repoUser, aggregates, err := es.PrepareCreateUser(ctx, user, policy, "") if err != nil { return nil, err } @@ -137,19 +138,18 @@ func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User) return model.UserToModel(repoUser), nil } -func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*model.User, []*es_models.Aggregate, error) { +func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, resourceOwner string) (*model.User, []*es_models.Aggregate, error) { user.SetEmailAsUsername() if !user.IsValid() || user.Password == nil || user.SecretString == "" { return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "user is invalid") } - //TODO: Check Uniqueness id, err := es.idGenerator.Next() if err != nil { return nil, nil, err } user.AggregateID = id - err = user.HashPasswordIfExisting(es.PasswordAlg, false) + err = user.HashPasswordIfExisting(policy, es.PasswordAlg, false) if err != nil { return nil, nil, err } @@ -165,8 +165,8 @@ func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_mod return repoUser, aggregates, err } -func (es *UserEventstore) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) { - repoUser, createAggregates, err := es.PrepareRegisterUser(ctx, user, resourceOwner) +func (es *UserEventstore) RegisterUser(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, resourceOwner string) (*usr_model.User, error) { + repoUser, createAggregates, err := es.PrepareRegisterUser(ctx, user, policy, resourceOwner) if err != nil { return nil, err } @@ -380,15 +380,15 @@ func (es *UserEventstore) setPasswordCheckResult(ctx context.Context, user *usr_ return nil } -func (es *UserEventstore) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) { +func (es *UserEventstore) SetOneTimePassword(ctx context.Context, policy *policy_model.PasswordComplexityPolicy, password *usr_model.Password) (*usr_model.Password, error) { user, err := es.UserByID(ctx, password.AggregateID) if err != nil { return nil, err } - return es.changedPassword(ctx, user, password.SecretString, true) + return es.changedPassword(ctx, user, policy, password.SecretString, true) } -func (es *UserEventstore) SetPassword(ctx context.Context, userID, code, password string) error { +func (es *UserEventstore) SetPassword(ctx context.Context, policy *policy_model.PasswordComplexityPolicy, userID, code, password string) error { user, err := es.UserByID(ctx, userID) if err != nil { return err @@ -399,11 +399,11 @@ func (es *UserEventstore) SetPassword(ctx context.Context, userID, code, passwor if err := crypto.VerifyCode(user.PasswordCode.CreationDate, user.PasswordCode.Expiry, user.PasswordCode.Code, code, es.PasswordVerificationCode); err != nil { return caos_errs.ThrowPreconditionFailed(err, "EVENT-sd6DF", "code invalid") } - _, err = es.changedPassword(ctx, user, password, false) + _, err = es.changedPassword(ctx, user, policy, password, false) return err } -func (es *UserEventstore) ChangePassword(ctx context.Context, userID, old, new string) (*usr_model.Password, error) { +func (es *UserEventstore) ChangePassword(ctx context.Context, policy *policy_model.PasswordComplexityPolicy, userID, old, new string) (*usr_model.Password, error) { user, err := es.UserByID(ctx, userID) if err != nil { return nil, err @@ -414,16 +414,16 @@ func (es *UserEventstore) ChangePassword(ctx context.Context, userID, old, new s if err := crypto.CompareHash(user.Password.SecretCrypto, []byte(old), es.PasswordAlg); err != nil { return nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-s56a3", "invalid password") } - return es.changedPassword(ctx, user, new, false) + return es.changedPassword(ctx, user, policy, new, false) } -func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.User, password string, onetime bool) (*usr_model.Password, error) { - //TODO: check password policy - secret, err := crypto.Hash([]byte(password), es.PasswordAlg) +func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.User, policy *policy_model.PasswordComplexityPolicy, password string, onetime bool) (*usr_model.Password, error) { + pw := &usr_model.Password{SecretString: password} + err := pw.HashPasswordIfExisting(policy, es.PasswordAlg, onetime) if err != nil { return nil, err } - repoPassword := &model.Password{Secret: secret, ChangeRequired: onetime} + repoPassword := model.PasswordFromModel(pw) repoUser := model.UserFromModel(user) agg := PasswordChangeAggregate(es.AggregateCreator(), repoUser, repoPassword) err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg) diff --git a/internal/user/repository/eventsourcing/eventstore_test.go b/internal/user/repository/eventsourcing/eventstore_test.go index c614cf6b2..bfade5940 100644 --- a/internal/user/repository/eventsourcing/eventstore_test.go +++ b/internal/user/repository/eventsourcing/eventstore_test.go @@ -2,6 +2,7 @@ package eventsourcing import ( "context" + policy_model "github.com/caos/zitadel/internal/policy/model" "net" "testing" "time" @@ -83,9 +84,10 @@ func TestUserByID(t *testing.T) { func TestCreateUser(t *testing.T) { ctrl := gomock.NewController(t) type args struct { - es *UserEventstore - ctx context.Context - user *model.User + es *UserEventstore + ctx context.Context + user *model.User + policy *policy_model.PasswordComplexityPolicy } type res struct { user *model.User @@ -113,6 +115,7 @@ func TestCreateUser(t *testing.T) { IsEmailVerified: true, }, }, + policy: &policy_model.PasswordComplexityPolicy{}, }, res: res{ user: &model.User{ObjectRoot: es_models.ObjectRoot{Sequence: 1}, @@ -143,6 +146,7 @@ func TestCreateUser(t *testing.T) { IsEmailVerified: true, }, }, + policy: &policy_model.PasswordComplexityPolicy{}, }, res: res{ user: &model.User{ObjectRoot: es_models.ObjectRoot{Sequence: 1}, @@ -178,6 +182,7 @@ func TestCreateUser(t *testing.T) { IsPhoneVerified: true, }, }, + policy: &policy_model.PasswordComplexityPolicy{}, }, res: res{ user: &model.User{ObjectRoot: es_models.ObjectRoot{Sequence: 1}, @@ -214,6 +219,7 @@ func TestCreateUser(t *testing.T) { IsEmailVerified: true, }, }, + policy: &policy_model.PasswordComplexityPolicy{}, }, res: res{ user: &model.User{ObjectRoot: es_models.ObjectRoot{Sequence: 1}, @@ -231,6 +237,18 @@ func TestCreateUser(t *testing.T) { }, { name: "create user invalid", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + user: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}}, + policy: &policy_model.PasswordComplexityPolicy{}, + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "create user policy nil", args: args{ es: GetMockManipulateUser(ctrl), ctx: auth.NewMockContext("orgID", "userID"), @@ -243,7 +261,7 @@ func TestCreateUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := tt.args.es.CreateUser(tt.args.ctx, tt.args.user) + result, err := tt.args.es.CreateUser(tt.args.ctx, tt.args.user, tt.args.policy) if tt.res.errFunc == nil && result.AggregateID == "" { t.Errorf("result has no id") @@ -275,6 +293,7 @@ func TestRegisterUser(t *testing.T) { ctx context.Context user *model.User resourceOwner string + policy *policy_model.PasswordComplexityPolicy } type res struct { user *model.User @@ -304,6 +323,7 @@ func TestRegisterUser(t *testing.T) { SecretString: "Password", }, }, + policy: &policy_model.PasswordComplexityPolicy{}, resourceOwner: "ResourceOwner", }, res: res{ @@ -336,6 +356,7 @@ func TestRegisterUser(t *testing.T) { SecretString: "Password", }, }, + policy: &policy_model.PasswordComplexityPolicy{}, resourceOwner: "ResourceOwner", }, res: res{ @@ -357,6 +378,7 @@ func TestRegisterUser(t *testing.T) { es: GetMockManipulateUser(ctrl), ctx: auth.NewMockContext("orgID", "userID"), user: &model.User{ObjectRoot: es_models.ObjectRoot{Sequence: 1}}, + policy: &policy_model.PasswordComplexityPolicy{}, resourceOwner: "ResourceOwner", }, res: res{ @@ -378,6 +400,7 @@ func TestRegisterUser(t *testing.T) { EmailAddress: "EmailAddress", }, }, + policy: &policy_model.PasswordComplexityPolicy{}, resourceOwner: "ResourceOwner", }, res: res{ @@ -386,6 +409,27 @@ func TestRegisterUser(t *testing.T) { }, { name: "no resourceowner", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + user: &model.User{ObjectRoot: es_models.ObjectRoot{Sequence: 1}, + Profile: &model.Profile{ + UserName: "EmailAddress", + FirstName: "FirstName", + LastName: "LastName", + }, + Email: &model.Email{ + EmailAddress: "EmailAddress", + }, + }, + policy: &policy_model.PasswordComplexityPolicy{}, + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "no policy", args: args{ es: GetMockManipulateUser(ctrl), ctx: auth.NewMockContext("orgID", "userID"), @@ -407,7 +451,7 @@ func TestRegisterUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := tt.args.es.RegisterUser(tt.args.ctx, tt.args.user, tt.args.resourceOwner) + result, err := tt.args.es.RegisterUser(tt.args.ctx, tt.args.user, tt.args.policy, tt.args.resourceOwner) if tt.res.errFunc == nil && result.AggregateID == "" { t.Errorf("result has no id") @@ -1029,6 +1073,7 @@ func TestSetOneTimePassword(t *testing.T) { type args struct { es *UserEventstore ctx context.Context + policy *policy_model.PasswordComplexityPolicy password *model.Password } type res struct { @@ -1045,6 +1090,7 @@ func TestSetOneTimePassword(t *testing.T) { args: args{ es: GetMockManipulateUserWithPasswordCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}}), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"}, }, res: res{ @@ -1056,6 +1102,7 @@ func TestSetOneTimePassword(t *testing.T) { args: args{ es: GetMockManipulateUser(ctrl), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: ""}, SecretString: "Password"}, }, res: res{ @@ -1067,6 +1114,7 @@ func TestSetOneTimePassword(t *testing.T) { args: args{ es: GetMockManipulateUserNoEvents(ctrl), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"}, }, res: res{ @@ -1076,7 +1124,7 @@ func TestSetOneTimePassword(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := tt.args.es.SetOneTimePassword(tt.args.ctx, tt.args.password) + result, err := tt.args.es.SetOneTimePassword(tt.args.ctx, tt.args.policy, tt.args.password) if tt.res.errFunc == nil && result.AggregateID == "" { t.Errorf("result has no id") @@ -1226,6 +1274,7 @@ func TestSetPassword(t *testing.T) { type args struct { es *UserEventstore ctx context.Context + policy *policy_model.PasswordComplexityPolicy userID string code string password string @@ -1253,6 +1302,7 @@ func TestSetPassword(t *testing.T) { }, ), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "userID", code: "code", password: "password", @@ -1264,6 +1314,7 @@ func TestSetPassword(t *testing.T) { args: args{ es: GetMockManipulateUser(ctrl), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "", code: "code", password: "password", @@ -1277,6 +1328,7 @@ func TestSetPassword(t *testing.T) { args: args{ es: GetMockManipulateUserNoEvents(ctrl), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "userID", code: "code", password: "password", @@ -1294,6 +1346,7 @@ func TestSetPassword(t *testing.T) { }, ), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "userID", code: "code", password: "password", @@ -1317,6 +1370,7 @@ func TestSetPassword(t *testing.T) { }, ), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "userID", code: "code", password: "password", @@ -1328,7 +1382,7 @@ func TestSetPassword(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.es.SetPassword(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.password) + err := tt.args.es.SetPassword(tt.args.ctx, tt.args.policy, tt.args.userID, tt.args.code, tt.args.password) if tt.res.errFunc == nil && err != nil { t.Errorf("result has error: %v", err) @@ -1345,6 +1399,7 @@ func TestChangePassword(t *testing.T) { type args struct { es *UserEventstore ctx context.Context + policy *policy_model.PasswordComplexityPolicy userID string old string new string @@ -1372,6 +1427,7 @@ func TestChangePassword(t *testing.T) { }, ), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "userID", old: "old", new: "new", @@ -1385,6 +1441,7 @@ func TestChangePassword(t *testing.T) { args: args{ es: GetMockManipulateUser(ctrl), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "", old: "old", new: "new", @@ -1398,6 +1455,7 @@ func TestChangePassword(t *testing.T) { args: args{ es: GetMockManipulateUserNoEvents(ctrl), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "userID", old: "old", new: "new", @@ -1415,6 +1473,7 @@ func TestChangePassword(t *testing.T) { }, ), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "userID", old: "old", new: "new", @@ -1437,6 +1496,7 @@ func TestChangePassword(t *testing.T) { }, ), ctx: auth.NewMockContext("orgID", "userID"), + policy: &policy_model.PasswordComplexityPolicy{}, userID: "userID", old: "old", new: "new", @@ -1445,10 +1505,32 @@ func TestChangePassword(t *testing.T) { errFunc: caos_errs.IsErrorInvalidArgument, }, }, + { + name: "no policy", + args: args{ + es: GetMockManipulateUserWithPasswordAndEmailCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + Password: &repo_model.Password{Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + Crypted: []byte("old"), + }}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + old: "old", + new: "new", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := tt.args.es.ChangePassword(tt.args.ctx, tt.args.userID, tt.args.old, tt.args.new) + result, err := tt.args.es.ChangePassword(tt.args.ctx, tt.args.policy, tt.args.userID, tt.args.old, tt.args.new) if tt.res.errFunc == nil && result.AggregateID == "" { t.Errorf("result has no id")