feat: user service v2 create, update and remove (#6996)

* feat: user service v2 remove user

* feat: user service v2 add user human

* feat: user service v2 change user human

* feat: user service v2 change user human unit tests

* feat: user service v2 reactivate, deactivate, lock, unlock user

* feat: user service v2 integration tests

* fix: merge back origin/main

* lint: linter corrections

* fix: move permission check for isVerfied and password change

* fix: add deprecated notices and other review comments

* fix: consistent naming in proto

* fix: errors package renaming

* fix: remove / delete user renaming in integration test

* fix: machine user status changes through user v2 api

* fix: linting changes

* fix: linting changes

* fix: changes from review

* fix: changes from review

* fix: changes from review

* fix: changes from review

* fix: changes from review

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2023-12-21 10:03:37 +01:00
committed by GitHub
parent acb604c045
commit a0a82b59e1
34 changed files with 9368 additions and 98 deletions

View File

@@ -11,7 +11,7 @@ import (
func (s *Server) UpdateMyPassword(ctx context.Context, req *auth_pb.UpdateMyPasswordRequest) (*auth_pb.UpdateMyPasswordResponse, error) {
ctxData := authz.GetCtxData(ctx)
objectDetails, err := s.command.ChangePassword(ctx, ctxData.ResourceOwner, ctxData.UserID, req.OldPassword, req.NewPassword, "")
objectDetails, err := s.command.ChangePassword(ctx, ctxData.ResourceOwner, ctxData.UserID, req.OldPassword, req.NewPassword)
if err != nil {
return nil, err
}

View File

@@ -53,9 +53,9 @@ func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest)
switch v := req.GetVerification().(type) {
case *user.SetPasswordRequest_CurrentPassword:
details, err = s.command.ChangePassword(ctx, resourceOwner, req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "")
details, err = s.command.ChangePassword(ctx, resourceOwner, req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword())
case *user.SetPasswordRequest_VerificationCode:
details, err = s.command.SetPasswordWithVerifyCode(ctx, resourceOwner, req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "")
details, err = s.command.SetPasswordWithVerifyCode(ctx, resourceOwner, req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword())
case nil:
details, err = s.command.SetPassword(ctx, resourceOwner, req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired())
default:

View File

@@ -28,7 +28,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest
return nil, err
}
orgID := authz.GetCtxData(ctx).OrgID
if err = s.command.AddHuman(ctx, orgID, human, false); err != nil {
if err = s.command.AddUserHuman(ctx, orgID, human, false, s.userCodeAlg); err != nil {
return nil, err
}
return &user.AddHumanUserResponse{
@@ -113,6 +113,172 @@ func genderToDomain(gender user.Gender) domain.Gender {
}
}
func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) {
human, err := UpdateUserRequestToChangeHuman(req)
if err != nil {
return nil, err
}
err = s.command.ChangeUserHuman(ctx, human, s.userCodeAlg)
if err != nil {
return nil, err
}
return &user.UpdateHumanUserResponse{
Details: object.DomainToDetailsPb(human.Details),
EmailCode: human.EmailCode,
PhoneCode: human.PhoneCode,
}, nil
}
func (s *Server) LockUser(ctx context.Context, req *user.LockUserRequest) (_ *user.LockUserResponse, err error) {
details, err := s.command.LockUserV2(ctx, req.UserId)
if err != nil {
return nil, err
}
return &user.LockUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) UnlockUser(ctx context.Context, req *user.UnlockUserRequest) (_ *user.UnlockUserResponse, err error) {
details, err := s.command.UnlockUserV2(ctx, req.UserId)
if err != nil {
return nil, err
}
return &user.UnlockUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeactivateUser(ctx context.Context, req *user.DeactivateUserRequest) (_ *user.DeactivateUserResponse, err error) {
details, err := s.command.DeactivateUserV2(ctx, req.UserId)
if err != nil {
return nil, err
}
return &user.DeactivateUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) ReactivateUser(ctx context.Context, req *user.ReactivateUserRequest) (_ *user.ReactivateUserResponse, err error) {
details, err := s.command.ReactivateUserV2(ctx, req.UserId)
if err != nil {
return nil, err
}
return &user.ReactivateUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p {
var pNil *p
if value == nil {
return pNil
}
pVal := conv(*value)
return &pVal
}
func UpdateUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) {
email, err := SetHumanEmailToEmail(req.Email, req.GetUserId())
if err != nil {
return nil, err
}
return &command.ChangeHuman{
ID: req.GetUserId(),
Username: req.Username,
Profile: SetHumanProfileToProfile(req.Profile),
Email: email,
Phone: SetHumanPhoneToPhone(req.Phone),
Password: SetHumanPasswordToPassword(req.Password),
}, nil
}
func SetHumanProfileToProfile(profile *user.SetHumanProfile) *command.Profile {
if profile == nil {
return nil
}
var firstName *string
if profile.GivenName != "" {
firstName = &profile.GivenName
}
var lastName *string
if profile.FamilyName != "" {
lastName = &profile.FamilyName
}
return &command.Profile{
FirstName: firstName,
LastName: lastName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make),
Gender: ifNotNilPtr(profile.Gender, genderToDomain),
}
}
func SetHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) {
if email == nil {
return nil, nil
}
var urlTemplate string
if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil {
urlTemplate = *email.GetSendCode().UrlTemplate
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil {
return nil, err
}
}
return &command.Email{
Address: domain.EmailAddress(email.Email),
Verified: email.GetIsVerified(),
ReturnCode: email.GetReturnCode() != nil,
URLTemplate: urlTemplate,
}, nil
}
func SetHumanPhoneToPhone(phone *user.SetHumanPhone) *command.Phone {
if phone == nil {
return nil
}
return &command.Phone{
Number: domain.PhoneNumber(phone.GetPhone()),
Verified: phone.GetIsVerified(),
ReturnCode: phone.GetReturnCode() != nil,
}
}
func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password {
if password == nil {
return nil
}
var changeRequired bool
var passwordStr *string
if password.GetPassword() != nil {
passwordStr = &password.GetPassword().Password
changeRequired = password.GetPassword().GetChangeRequired()
}
var hash *string
if password.GetHashedPassword() != nil {
hash = &password.GetHashedPassword().Hash
changeRequired = password.GetHashedPassword().GetChangeRequired()
}
var code *string
if password.GetVerificationCode() != "" {
codeT := password.GetVerificationCode()
code = &codeT
}
var oldPassword *string
if password.GetCurrentPassword() != "" {
oldPasswordT := password.GetCurrentPassword()
oldPassword = &oldPasswordT
}
return &command.Password{
PasswordCode: code,
OldPassword: oldPassword,
Password: passwordStr,
EncodedPasswordHash: hash,
ChangeRequired: changeRequired,
}
}
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
orgID := authz.GetCtxData(ctx).OrgID
details, err := s.command.AddUserIDPLink(ctx, req.UserId, orgID, &command.AddLink{
@@ -128,6 +294,92 @@ func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_
}, nil
}
func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) {
memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId())
if err != nil {
return nil, err
}
details, err := s.command.RemoveUserV2(ctx, req.UserId, memberships, grants...)
if err != nil {
return nil, err
}
return &user.DeleteUserResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
if err != nil {
return nil, nil, err
}
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{userGrantUserQuery},
}, true, true)
if err != nil {
return nil, nil, err
}
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
if err != nil {
return nil, nil, err
}
memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{
Queries: []query.SearchQuery{membershipsUserQuery},
}, false)
if err != nil {
return nil, nil, err
}
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), nil
}
func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership {
cascades := make([]*command.CascadingMembership, len(memberships))
for i, membership := range memberships {
cascades[i] = &command.CascadingMembership{
UserID: membership.UserID,
ResourceOwner: membership.ResourceOwner,
IAM: cascadingIAMMembership(membership.IAM),
Org: cascadingOrgMembership(membership.Org),
Project: cascadingProjectMembership(membership.Project),
ProjectGrant: cascadingProjectGrantMembership(membership.ProjectGrant),
}
}
return cascades
}
func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingIAMMembership {
if membership == nil {
return nil
}
return &command.CascadingIAMMembership{IAMID: membership.IAMID}
}
func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership {
if membership == nil {
return nil
}
return &command.CascadingOrgMembership{OrgID: membership.OrgID}
}
func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership {
if membership == nil {
return nil
}
return &command.CascadingProjectMembership{ProjectID: membership.ProjectID}
}
func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership {
if membership == nil {
return nil
}
return &command.CascadingProjectGrantMembership{ProjectID: membership.ProjectID, GrantID: membership.GrantID}
}
func userGrantsToIDs(userGrants []*query.UserGrant) []string {
converted := make([]string, len(userGrants))
for i, grant := range userGrants {
converted[i] = grant.ID
}
return converted
}
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) {
switch t := req.GetContent().(type) {
case *user.StartIdentityProviderIntentRequest_Urls:

View File

@@ -540,6 +540,896 @@ func TestServer_AddHumanUser(t *testing.T) {
if tt.want.GetEmailCode() != "" {
assert.NotEmpty(t, got.GetEmailCode())
}
if tt.want.GetPhoneCode() != "" {
assert.NotEmpty(t, got.GetPhoneCode())
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_UpdateHumanUser(t *testing.T) {
type args struct {
ctx context.Context
req *user.UpdateHumanUserRequest
}
tests := []struct {
name string
prepare func(request *user.UpdateHumanUserRequest) error
args args
want *user.UpdateHumanUserResponse
wantErr bool
}{
{
name: "not exisiting",
prepare: func(request *user.UpdateHumanUserRequest) error {
request.UserId = "notexisiting"
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Username: gu.Ptr("changed"),
},
},
wantErr: true,
},
{
name: "change username, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Username: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "change profile, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "change email, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Email: &user.SetHumanEmail{
Email: "changed@test.com",
Verification: &user.SetHumanEmail_IsVerified{IsVerified: true},
},
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "change email, code, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Email: &user.SetHumanEmail{
Email: "changed@test.com",
Verification: &user.SetHumanEmail_ReturnCode{},
},
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
EmailCode: gu.Ptr("something"),
},
},
{
name: "change phone, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Phone: &user.SetHumanPhone{
Phone: "+41791234567",
Verification: &user.SetHumanPhone_IsVerified{IsVerified: true},
},
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "change phone, code, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Phone: &user.SetHumanPhone{
Phone: "+41791234568",
Verification: &user.SetHumanPhone_ReturnCode{},
},
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
PhoneCode: gu.Ptr("something"),
},
},
{
name: "change password, code, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{
UserId: userID,
Medium: &user.PasswordResetRequest_ReturnCode{
ReturnCode: &user.ReturnPasswordResetCode{},
},
})
if err != nil {
return err
}
request.Password.Verification = &user.SetPassword_VerificationCode{
VerificationCode: resp.GetVerificationCode(),
}
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Password: &user.SetPassword{
PasswordType: &user.SetPassword_Password{
Password: &user.Password{
Password: "Password1!",
ChangeRequired: true,
},
},
},
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "change hashed password, code, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{
UserId: userID,
Medium: &user.PasswordResetRequest_ReturnCode{
ReturnCode: &user.ReturnPasswordResetCode{},
},
})
if err != nil {
return err
}
request.Password.Verification = &user.SetPassword_VerificationCode{
VerificationCode: resp.GetVerificationCode(),
}
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Password: &user.SetPassword{
PasswordType: &user.SetPassword_HashedPassword{
HashedPassword: &user.HashedPassword{
Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm",
},
},
},
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "change hashed password, code, not supported",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{
UserId: userID,
Medium: &user.PasswordResetRequest_ReturnCode{
ReturnCode: &user.ReturnPasswordResetCode{},
},
})
if err != nil {
return err
}
request.Password = &user.SetPassword{
Verification: &user.SetPassword_VerificationCode{
VerificationCode: resp.GetVerificationCode(),
},
}
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Password: &user.SetPassword{
PasswordType: &user.SetPassword_HashedPassword{
HashedPassword: &user.HashedPassword{
Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ",
},
},
},
},
},
wantErr: true,
},
{
name: "change password, old password, ok",
prepare: func(request *user.UpdateHumanUserRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{
UserId: userID,
Medium: &user.PasswordResetRequest_ReturnCode{
ReturnCode: &user.ReturnPasswordResetCode{},
},
})
if err != nil {
return err
}
pw := "Password1."
_, err = Client.SetPassword(CTX, &user.SetPasswordRequest{
UserId: userID,
NewPassword: &user.Password{
Password: pw,
ChangeRequired: true,
},
Verification: &user.SetPasswordRequest_VerificationCode{
VerificationCode: resp.GetVerificationCode(),
},
})
if err != nil {
return err
}
request.Password.Verification = &user.SetPassword_CurrentPassword{
CurrentPassword: pw,
}
return nil
},
args: args{
CTX,
&user.UpdateHumanUserRequest{
Password: &user.SetPassword{
PasswordType: &user.SetPassword_Password{
Password: &user.Password{
Password: "Password1!",
ChangeRequired: true,
},
},
},
},
},
want: &user.UpdateHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if tt.want.GetEmailCode() != "" {
assert.NotEmpty(t, got.GetEmailCode())
}
if tt.want.GetPhoneCode() != "" {
assert.NotEmpty(t, got.GetPhoneCode())
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_LockUser(t *testing.T) {
type args struct {
ctx context.Context
req *user.LockUserRequest
prepare func(request *user.LockUserRequest) error
}
tests := []struct {
name string
args args
want *user.LockUserResponse
wantErr bool
}{
{
name: "lock, not existing",
args: args{
CTX,
&user.LockUserRequest{
UserId: "notexisting",
},
func(request *user.LockUserRequest) error { return nil },
},
wantErr: true,
},
{
name: "lock, ok",
args: args{
CTX,
&user.LockUserRequest{},
func(request *user.LockUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
want: &user.LockUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "lock machine, ok",
args: args{
CTX,
&user.LockUserRequest{},
func(request *user.LockUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
want: &user.LockUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "lock, already locked",
args: args{
CTX,
&user.LockUserRequest{},
func(request *user.LockUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
_, err := Client.LockUser(CTX, &user.LockUserRequest{
UserId: resp.GetUserId(),
})
return err
},
},
wantErr: true,
},
{
name: "lock machine, already locked",
args: args{
CTX,
&user.LockUserRequest{},
func(request *user.LockUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
_, err := Client.LockUser(CTX, &user.LockUserRequest{
UserId: resp.GetUserId(),
})
return err
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.LockUser(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_UnLockUser(t *testing.T) {
type args struct {
ctx context.Context
req *user.UnlockUserRequest
prepare func(request *user.UnlockUserRequest) error
}
tests := []struct {
name string
args args
want *user.UnlockUserResponse
wantErr bool
}{
{
name: "unlock, not existing",
args: args{
CTX,
&user.UnlockUserRequest{
UserId: "notexisting",
},
func(request *user.UnlockUserRequest) error { return nil },
},
wantErr: true,
},
{
name: "unlock, not locked",
args: args{
ctx: CTX,
req: &user.UnlockUserRequest{},
prepare: func(request *user.UnlockUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
wantErr: true,
},
{
name: "unlock machine, not locked",
args: args{
ctx: CTX,
req: &user.UnlockUserRequest{},
prepare: func(request *user.UnlockUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
wantErr: true,
},
{
name: "unlock, ok",
args: args{
ctx: CTX,
req: &user.UnlockUserRequest{},
prepare: func(request *user.UnlockUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
_, err := Client.LockUser(CTX, &user.LockUserRequest{
UserId: resp.GetUserId(),
})
return err
},
},
want: &user.UnlockUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "unlock machine, ok",
args: args{
ctx: CTX,
req: &user.UnlockUserRequest{},
prepare: func(request *user.UnlockUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
_, err := Client.LockUser(CTX, &user.LockUserRequest{
UserId: resp.GetUserId(),
})
return err
},
},
want: &user.UnlockUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.UnlockUser(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_DeactivateUser(t *testing.T) {
type args struct {
ctx context.Context
req *user.DeactivateUserRequest
prepare func(request *user.DeactivateUserRequest) error
}
tests := []struct {
name string
args args
want *user.DeactivateUserResponse
wantErr bool
}{
{
name: "deactivate, not existing",
args: args{
CTX,
&user.DeactivateUserRequest{
UserId: "notexisting",
},
func(request *user.DeactivateUserRequest) error { return nil },
},
wantErr: true,
},
{
name: "deactivate, ok",
args: args{
CTX,
&user.DeactivateUserRequest{},
func(request *user.DeactivateUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
want: &user.DeactivateUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "deactivate machine, ok",
args: args{
CTX,
&user.DeactivateUserRequest{},
func(request *user.DeactivateUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
want: &user.DeactivateUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "deactivate, already deactivated",
args: args{
CTX,
&user.DeactivateUserRequest{},
func(request *user.DeactivateUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
_, err := Client.DeactivateUser(CTX, &user.DeactivateUserRequest{
UserId: resp.GetUserId(),
})
return err
},
},
wantErr: true,
},
{
name: "deactivate machine, already deactivated",
args: args{
CTX,
&user.DeactivateUserRequest{},
func(request *user.DeactivateUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
_, err := Client.DeactivateUser(CTX, &user.DeactivateUserRequest{
UserId: resp.GetUserId(),
})
return err
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.DeactivateUser(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_ReactivateUser(t *testing.T) {
type args struct {
ctx context.Context
req *user.ReactivateUserRequest
prepare func(request *user.ReactivateUserRequest) error
}
tests := []struct {
name string
args args
want *user.ReactivateUserResponse
wantErr bool
}{
{
name: "reactivate, not existing",
args: args{
CTX,
&user.ReactivateUserRequest{
UserId: "notexisting",
},
func(request *user.ReactivateUserRequest) error { return nil },
},
wantErr: true,
},
{
name: "reactivate, not deactivated",
args: args{
ctx: CTX,
req: &user.ReactivateUserRequest{},
prepare: func(request *user.ReactivateUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
wantErr: true,
},
{
name: "reactivate machine, not deactivated",
args: args{
ctx: CTX,
req: &user.ReactivateUserRequest{},
prepare: func(request *user.ReactivateUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
wantErr: true,
},
{
name: "reactivate, ok",
args: args{
ctx: CTX,
req: &user.ReactivateUserRequest{},
prepare: func(request *user.ReactivateUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
_, err := Client.DeactivateUser(CTX, &user.DeactivateUserRequest{
UserId: resp.GetUserId(),
})
return err
},
},
want: &user.ReactivateUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "reactivate machine, ok",
args: args{
ctx: CTX,
req: &user.ReactivateUserRequest{},
prepare: func(request *user.ReactivateUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
_, err := Client.DeactivateUser(CTX, &user.DeactivateUserRequest{
UserId: resp.GetUserId(),
})
return err
},
},
want: &user.ReactivateUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.ReactivateUser(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_DeleteUser(t *testing.T) {
projectResp, err := Tester.CreateProject(CTX)
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.DeleteUserRequest
prepare func(request *user.DeleteUserRequest) error
}
tests := []struct {
name string
args args
want *user.DeleteUserResponse
wantErr bool
}{
{
name: "remove, not existing",
args: args{
CTX,
&user.DeleteUserRequest{
UserId: "notexisting",
},
func(request *user.DeleteUserRequest) error { return nil },
},
wantErr: true,
},
{
name: "remove human, ok",
args: args{
ctx: CTX,
req: &user.DeleteUserRequest{},
prepare: func(request *user.DeleteUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return err
},
},
want: &user.DeleteUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "remove machine, ok",
args: args{
ctx: CTX,
req: &user.DeleteUserRequest{},
prepare: func(request *user.DeleteUserRequest) error {
resp := Tester.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
return err
},
},
want: &user.DeleteUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "remove dependencies, ok",
args: args{
ctx: CTX,
req: &user.DeleteUserRequest{},
prepare: func(request *user.DeleteUserRequest) error {
resp := Tester.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
Tester.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId)
Tester.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId)
Tester.CreateOrgMembership(t, CTX, request.UserId)
return err
},
},
want: &user.DeleteUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.DeleteUser(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}

View File

@@ -4,8 +4,6 @@ import (
"net/http"
"github.com/zitadel/zitadel/internal/domain"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
)
const (
@@ -26,8 +24,7 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
l.renderError(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err = l.command.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserOrgID, authReq.UserID, data.OldPassword, data.NewPassword, userAgentID)
_, err = l.command.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserOrgID, authReq.UserID, data.OldPassword, data.NewPassword)
if err != nil {
l.renderChangePassword(w, r, authReq, err)
return

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"net/http"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -72,8 +71,7 @@ func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *dom
if authReq != nil {
userOrg = authReq.UserOrgID
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err := l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password, userAgentID)
_, err := l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password)
if err != nil {
l.renderInitPassword(w, r, authReq, data.UserID, "", err)
return

View File

@@ -44,7 +44,7 @@ func (c *Commands) ChangeDefaultDomainPolicy(ctx context.Context, userLoginMustB
}
func (c *Commands) getDefaultDomainPolicy(ctx context.Context) (*domain.DomainPolicy, error) {
policyWriteModel, err := c.defaultDomainPolicyWriteModelByID(ctx)
policyWriteModel, err := c.instanceDomainPolicyWriteModel(ctx)
if err != nil {
return nil, err
}
@@ -56,7 +56,7 @@ func (c *Commands) getDefaultDomainPolicy(ctx context.Context) (*domain.DomainPo
return policy, nil
}
func (c *Commands) defaultDomainPolicyWriteModelByID(ctx context.Context) (policy *InstanceDomainPolicyWriteModel, err error) {
func (c *Commands) instanceDomainPolicyWriteModel(ctx context.Context) (policy *InstanceDomainPolicyWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@@ -453,7 +453,7 @@ func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation {
return nil, zerrors.ThrowNotFound(nil, "COMMA-aps2n", "Errors.Org.NotFound")
}
domainPolicy, err := c.getOrgDomainPolicy(ctx, a.ID)
domainPolicy, err := c.domainPolicyWriteModel(ctx, a.ID)
if err != nil {
return nil, err
}

View File

@@ -59,18 +59,19 @@ func (c *Commands) RemoveOrgDomainPolicy(ctx context.Context, orgID string) (*do
return pushedEventsToObjectDetails(pushedEvents), nil
}
// Deprecated: Use commands.domainPolicyWriteModel directly, to remove the domain.DomainPolicy struct
func (c *Commands) getOrgDomainPolicy(ctx context.Context, orgID string) (*domain.DomainPolicy, error) {
policy, err := c.orgDomainPolicyWriteModelByID(ctx, orgID)
policy, err := c.orgDomainPolicyWriteModel(ctx, orgID)
if err != nil {
return nil, err
}
if policy.State == domain.PolicyStateActive {
if policy.State.Exists() {
return orgWriteModelToDomainPolicy(policy), nil
}
return c.getDefaultDomainPolicy(ctx)
}
func (c *Commands) orgDomainPolicyWriteModelByID(ctx context.Context, orgID string) (policy *OrgDomainPolicyWriteModel, err error) {
func (c *Commands) orgDomainPolicyWriteModel(ctx context.Context, orgID string) (policy *OrgDomainPolicyWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@@ -26,7 +26,7 @@ type UniqueConstraintReadModel struct {
}
type commandProvider interface {
getOrgDomainPolicy(ctx context.Context, orgID string) (*domain.DomainPolicy, error)
domainPolicyWriteModel(ctx context.Context, orgID string) (*PolicyDomainWriteModel, error)
}
func NewUniqueConstraintReadModel(ctx context.Context, provider commandProvider) *UniqueConstraintReadModel {
@@ -114,21 +114,21 @@ func (rm *UniqueConstraintReadModel) Reduce() error {
case *project.RoleRemovedEvent:
rm.removeUniqueConstraint(e.Aggregate().ID, e.Key, project.UniqueRoleType)
case *user.HumanAddedEvent:
policy, err := rm.commandProvider.getOrgDomainPolicy(rm.ctx, e.Aggregate().ResourceOwner)
policy, err := rm.commandProvider.domainPolicyWriteModel(rm.ctx, e.Aggregate().ResourceOwner)
if err != nil {
logging.Log("COMMAND-0k9Gs").WithError(err).Error("could not read policy for human added event unique constraint")
continue
}
rm.addUniqueConstraint(e.Aggregate().ID, e.Aggregate().ID, user.NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, policy.UserLoginMustBeDomain))
case *user.HumanRegisteredEvent:
policy, err := rm.commandProvider.getOrgDomainPolicy(rm.ctx, e.Aggregate().ResourceOwner)
policy, err := rm.commandProvider.domainPolicyWriteModel(rm.ctx, e.Aggregate().ResourceOwner)
if err != nil {
logging.Log("COMMAND-m9fod").WithError(err).Error("could not read policy for human registered event unique constraint")
continue
}
rm.addUniqueConstraint(e.Aggregate().ID, e.Aggregate().ID, user.NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, policy.UserLoginMustBeDomain))
case *user.MachineAddedEvent:
policy, err := rm.commandProvider.getOrgDomainPolicy(rm.ctx, e.Aggregate().ResourceOwner)
policy, err := rm.commandProvider.domainPolicyWriteModel(rm.ctx, e.Aggregate().ResourceOwner)
if err != nil {
logging.Log("COMMAND-2n8vs").WithError(err).Error("could not read policy for machine added event unique constraint")
continue
@@ -138,14 +138,14 @@ func (rm *UniqueConstraintReadModel) Reduce() error {
rm.removeUniqueConstraint(e.Aggregate().ID, e.Aggregate().ID, user.UniqueUsername)
rm.listRemoveUniqueConstraint(e.Aggregate().ID, user.UniqueUserIDPLinkType)
case *user.UsernameChangedEvent:
policy, err := rm.commandProvider.getOrgDomainPolicy(rm.ctx, e.Aggregate().ResourceOwner)
policy, err := rm.commandProvider.domainPolicyWriteModel(rm.ctx, e.Aggregate().ResourceOwner)
if err != nil {
logging.Log("COMMAND-5n8gk").WithError(err).Error("could not read policy for username changed event unique constraint")
continue
}
rm.changeUniqueConstraint(e.Aggregate().ID, e.Aggregate().ID, user.NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, policy.UserLoginMustBeDomain))
case *user.DomainClaimedEvent:
policy, err := rm.commandProvider.getOrgDomainPolicy(rm.ctx, e.Aggregate().ResourceOwner)
policy, err := rm.commandProvider.domainPolicyWriteModel(rm.ctx, e.Aggregate().ResourceOwner)
if err != nil {
logging.Log("COMMAND-xb8uf").WithError(err).Error("could not read policy for domain claimed event unique constraint")
continue

View File

@@ -38,7 +38,7 @@ func (c *Commands) ChangeUsername(ctx context.Context, orgID, userID, userName s
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-6m9gs", "Errors.User.UsernameNotChanged")
}
domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID)
domainPolicy, err := c.domainPolicyWriteModel(ctx, orgID)
if err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-38fnu", "Errors.Org.DomainPolicy.NotExisting")
}
@@ -196,7 +196,7 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string,
return nil, zerrors.ThrowNotFound(nil, "COMMAND-m9od", "Errors.User.NotFound")
}
domainPolicy, err := c.getOrgDomainPolicy(ctx, existingUser.ResourceOwner)
domainPolicy, err := c.domainPolicyWriteModel(ctx, existingUser.ResourceOwner)
if err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotExisting")
}
@@ -330,7 +330,7 @@ func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events
changedUserGrant := NewUserWriteModel(userID, existingUser.ResourceOwner)
userAgg := UserAggregateFromWriteModel(&changedUserGrant.WriteModel)
domainPolicy, err := c.getOrgDomainPolicy(ctx, existingUser.ResourceOwner)
domainPolicy, err := c.domainPolicyWriteModel(ctx, existingUser.ResourceOwner)
if err != nil {
return nil, nil, err
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
// Deprecated: User commands.domainPolicyWriteModel directly, to remove use of eventstore.Filter function
func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, orgID string) (*PolicyDomainWriteModel, error) {
wm, err := orgDomainPolicy(ctx, filter, orgID)
if err != nil {
@@ -25,6 +26,25 @@ func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQuer
return nil, zerrors.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")
}
func (c *Commands) domainPolicyWriteModel(ctx context.Context, orgID string) (*PolicyDomainWriteModel, error) {
wm, err := c.orgDomainPolicyWriteModel(ctx, orgID)
if err != nil {
return nil, err
}
if wm != nil && wm.State.Exists() {
return &wm.PolicyDomainWriteModel, err
}
instanceWriteModel, err := c.instanceDomainPolicyWriteModel(ctx)
if err != nil {
return nil, err
}
if instanceWriteModel != nil && instanceWriteModel.State.Exists() {
return &instanceWriteModel.PolicyDomainWriteModel, err
}
return nil, zerrors.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")
}
// Deprecated: Use commands.orgDomainPolicyWriteModel directly, to remove use of eventstore.Filter function
func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer, orgID string) (*OrgDomainPolicyWriteModel, error) {
policy := NewOrgDomainPolicyWriteModel(orgID)
events, err := filter(ctx, policy.Query())
@@ -39,6 +59,7 @@ func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReduce
return policy, err
}
// Deprecated: Use commands.instanceDomainPolicyWriteModel directly, to remove use of eventstore.Filter function
func instanceDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*InstanceDomainPolicyWriteModel, error) {
policy := NewInstanceDomainPolicyWriteModel(ctx)
events, err := filter(ctx, policy.Query())

View File

@@ -127,6 +127,7 @@ func (m *AddMetadataEntry) Valid() error {
return nil
}
// Deprecated: use commands.AddUserHuman
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool) (err error) {
if resourceOwner == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
@@ -180,7 +181,7 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, hasher *crypto
return nil, err
}
if err = userValidateDomain(ctx, a, human.Username, domainPolicy.UserLoginMustBeDomain, filter); err != nil {
if err = c.userValidateDomain(ctx, a.ResourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil {
return nil, err
}
@@ -310,6 +311,7 @@ func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.
return append(cmds, user.NewHumanPhoneCodeAddedEventV2(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry, human.Phone.ReturnCode)), nil
}
// Deprecated: use commands.NewUserHumanWriteModel, to remove deprecated eventstore.Filter
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, human *AddHuman, orgID string) (err error) {
if human.ID == "" {
human.ID, err = c.idGenerator.Next()
@@ -347,7 +349,7 @@ func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQue
return nil
}
func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error {
func (c *Commands) userValidateDomain(ctx context.Context, resourceOwner string, username string, mustBeDomain bool) error {
if mustBeDomain {
return nil
}
@@ -357,17 +359,12 @@ func userValidateDomain(ctx context.Context, a *user.Aggregate, username string,
return nil
}
domainCheck := NewOrgDomainVerifiedWriteModel(username[index+1:])
events, err := filter(ctx, domainCheck.Query())
domainCheck, err := c.orgDomainVerifiedWriteModel(ctx, username[index+1:])
if err != nil {
return err
}
domainCheck.AppendEvents(events...)
if err = domainCheck.Reduce(); err != nil {
return err
}
if domainCheck.Verified && domainCheck.ResourceOwner != a.ResourceOwner {
if domainCheck.Verified && domainCheck.ResourceOwner != resourceOwner {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")
}
@@ -411,6 +408,7 @@ func (h *AddHuman) shouldAddInitCode() bool {
h.Password == ""
}
// Deprecated: use commands.AddUserHuman
func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) {
if orgID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
@@ -459,6 +457,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.
return writeModelToHuman(addedHuman), passwordlessCode, nil
}
// Deprecated: use commands.AddUserHuman
func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgMemberRoles []string, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (*domain.Human, error) {
if orgID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-GEdf2", "Errors.ResourceOwnerMissing")

View File

@@ -80,9 +80,7 @@ func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwne
commands = append(commands, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
}
if password != "" {
passwordWriteModel := NewHumanPasswordWriteModel(userID, existingCode.ResourceOwner)
passwordWriteModel.UserState = domain.UserStateActive
passwordCommand, err := c.setPasswordCommand(ctx, passwordWriteModel, password, false)
passwordCommand, err := c.setPasswordCommand(ctx, userAgg, domain.UserStateActive, password, false, false)
if err != nil {
return err
}

View File

@@ -78,7 +78,7 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname")
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-55M9f", "Errors.Org.NotFound")
}
orgPolicy, err := c.getOrgDomainPolicy(ctx, org.AggregateID)
orgPolicy, err := c.domainPolicyWriteModel(ctx, org.AggregateID)
if err != nil {
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname")
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound")

View File

@@ -34,7 +34,7 @@ func (c *Commands) SetPassword(ctx context.Context, orgID, userID, password stri
return c.setPassword(ctx, wm, password, oneTime)
}
func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, password, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, password string) (objectDetails *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -61,8 +61,10 @@ func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID,
return c.setPassword(ctx, wm, password, false)
}
func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
command, err := c.setPasswordCommand(ctx, wm, password, changeRequired)
// setEncodedPassword add change event from already encoded password to HumanPasswordWriteModel and return the necessary object details for response
func (c *Commands) setEncodedPassword(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
agg := user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, changeRequired, true)
if err != nil {
return nil, err
}
@@ -73,20 +75,39 @@ func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel,
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) setPasswordCommand(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (_ eventstore.Command, err error) {
if err = c.canUpdatePassword(ctx, password, wm); err != nil {
// setPassword add change event to HumanPasswordWriteModel and return the necessary object details for response
func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
agg := user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
command, err := c.setPasswordCommand(ctx, &agg.Aggregate, wm.UserState, password, changeRequired, false)
if err != nil {
return nil, err
}
ctx, span := tracing.NewNamedSpan(ctx, "passwap.Hash")
encoded, err := c.userPasswordHasher.Hash(password)
span.EndWithError(err)
if err = convertPasswapErr(err); err != nil {
err = c.pushAppendAndReduce(ctx, wm, command)
if err != nil {
return nil, err
}
return user.NewHumanPasswordChangedEvent(ctx, UserAggregateFromWriteModel(&wm.WriteModel), encoded, changeRequired, ""), nil
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
func (c *Commands) setPasswordCommand(ctx context.Context, agg *eventstore.Aggregate, userState domain.UserState, password string, changeRequired, encoded bool) (_ eventstore.Command, err error) {
if err = c.canUpdatePassword(ctx, password, agg.ResourceOwner, userState); err != nil {
return nil, err
}
if !encoded {
ctx, span := tracing.NewNamedSpan(ctx, "passwap.Hash")
encodedPassword, err := c.userPasswordHasher.Hash(password)
span.EndWithError(err)
if err = convertPasswapErr(err); err != nil {
return nil, err
}
return user.NewHumanPasswordChangedEvent(ctx, agg, encodedPassword, changeRequired, ""), nil
}
return user.NewHumanPasswordChangedEvent(ctx, agg, password, changeRequired, ""), nil
}
// ChangePassword change password of existing user
func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword string) (objectDetails *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -100,38 +121,39 @@ func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPasswor
if err != nil {
return nil, err
}
if wm.EncodedHash == "" {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Fds3s", "Errors.User.Password.Empty")
}
if err = c.canUpdatePassword(ctx, newPassword, wm); err != nil {
return nil, err
}
ctx, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.VerifyAndUpdate")
updated, err := c.userPasswordHasher.VerifyAndUpdate(wm.EncodedHash, oldPassword, newPassword)
spanPasswap.EndWithError(err)
if err = convertPasswapErr(err); err != nil {
return nil, err
}
err = c.pushAppendAndReduce(ctx, wm,
user.NewHumanPasswordChangedEvent(ctx, UserAggregateFromWriteModel(&wm.WriteModel), updated, false, userAgentID))
newPasswordHash, err := c.verifyAndUpdatePassword(ctx, wm.EncodedHash, oldPassword, newPassword)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
return c.setEncodedPassword(ctx, wm, newPasswordHash, false)
}
func (c *Commands) canUpdatePassword(ctx context.Context, newPassword string, wm *HumanPasswordWriteModel) (err error) {
// verifyAndUpdatePassword verify if the old password is correct with the encoded hash and
// returns the hash of the new password if so
func (c *Commands) verifyAndUpdatePassword(ctx context.Context, encodedHash, oldPassword, newPassword string) (string, error) {
if encodedHash == "" {
return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-Fds3s", "Errors.User.Password.NotSet")
}
_, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.Verify")
updated, err := c.userPasswordHasher.VerifyAndUpdate(encodedHash, oldPassword, newPassword)
spanPasswap.EndWithError(err)
return updated, convertPasswapErr(err)
}
// canUpdatePassword checks uf the given password can be used to be the password of a user
func (c *Commands) canUpdatePassword(ctx context.Context, newPassword string, resourceOwner string, state domain.UserState) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
if !isUserStateExists(state) {
return zerrors.ThrowNotFound(nil, "COMMAND-G8dh3", "Errors.User.Password.NotFound")
}
if wm.UserState == domain.UserStateInitial {
if state == domain.UserStateInitial {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-M9dse", "Errors.User.NotInitialised")
}
policy, err := c.getOrgPasswordComplexityPolicy(ctx, wm.ResourceOwner)
policy, err := c.getOrgPasswordComplexityPolicy(ctx, resourceOwner)
if err != nil {
return err
}
@@ -142,6 +164,7 @@ func (c *Commands) canUpdatePassword(ctx context.Context, newPassword string, wm
return nil
}
// RequestSetPassword generate and send out new code to change password for a specific user
func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, passwordVerificationCode crypto.Generator) (objectDetails *domain.ObjectDetails, err error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-M00oL", "Errors.User.UserIDMissing")
@@ -151,7 +174,7 @@ func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner
if err != nil {
return nil, err
}
if existingHuman.UserState == domain.UserStateUnspecified || existingHuman.UserState == domain.UserStateDeleted {
if !isUserStateExists(existingHuman.UserState) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Hj9ds", "Errors.User.NotFound")
}
if existingHuman.UserState == domain.UserStateInitial {
@@ -173,6 +196,7 @@ func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner
return writeModelToObjectDetails(&existingHuman.WriteModel), nil
}
// PasswordCodeSent notification send with code to change password
func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string) (err error) {
if userID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-meEfe", "Errors.User.UserIDMissing")
@@ -190,6 +214,7 @@ func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string) (
return err
}
// PasswordChangeSent notification sent that user changed his password
func (c *Commands) PasswordChangeSent(ctx context.Context, orgID, userID string) (err error) {
if userID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-pqlm2n", "Errors.User.UserIDMissing")
@@ -207,6 +232,7 @@ func (c *Commands) PasswordChangeSent(ctx context.Context, orgID, userID string)
return err
}
// HumanCheckPassword check password for user with additional informations from authRequest
func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, password string, authRequest *domain.AuthRequest, lockoutPolicy *domain.LockoutPolicy) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -230,13 +256,13 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo
if err != nil {
return err
}
if wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
if !isUserStateExists(wm.UserState) {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound")
}
if wm.UserState == domain.UserStateLocked {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-JLK35", "Errors.User.Locked")
}
if wm.EncodedHash == "" {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-3nJ4t", "Errors.User.Password.NotSet")
}

View File

@@ -278,7 +278,6 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
code string
resourceOwner string
password string
agentID string
}
type res struct {
want *domain.ObjectDetails
@@ -505,7 +504,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
userPasswordHasher: tt.fields.userPasswordHasher,
userEncryption: tt.fields.userEncryption,
}
got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID)
got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -529,7 +528,6 @@ func TestCommandSide_ChangePassword(t *testing.T) {
resourceOwner string
oldPassword string
newPassword string
agentID string
}
type res struct {
want *domain.ObjectDetails
@@ -675,18 +673,6 @@ func TestCommandSide_ChangePassword(t *testing.T) {
false,
"")),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -766,7 +752,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
eventstore: eventstoreExpect(t, tt.expect...),
userPasswordHasher: tt.fields.userPasswordHasher,
}
got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword, tt.args.agentID)
got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@@ -4266,6 +4266,17 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
}
}
func newAddMachineEvent(userLoginMustBeDomain bool, accessTokenType domain.OIDCTokenType) *user.MachineAddedEvent {
return user.NewMachineAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"name",
"description",
userLoginMustBeDomain,
accessTokenType,
)
}
func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain bool, phone string, preferredLanguage language.Tag) *user.HumanAddedEvent {
event := user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,

View File

@@ -149,7 +149,7 @@ func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner,
if err != nil {
return nil, nil, nil, err
}
orgPolicy, err := c.getOrgDomainPolicy(ctx, org.AggregateID)
orgPolicy, err := c.domainPolicyWriteModel(ctx, org.AggregateID)
if err != nil {
return nil, nil, nil, err
}

213
internal/command/user_v2.go Normal file
View File

@@ -0,0 +1,213 @@
package command
import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) LockUserV2(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-agz3eczifm", "Errors.User.UserIDMissing")
}
existingHuman, err := c.userStateWriteModel(ctx, userID)
if err != nil {
return nil, err
}
if !isUserStateExists(existingHuman.UserState) {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-450yxuqrh1", "Errors.User.NotFound")
}
if !hasUserState(existingHuman.UserState, domain.UserStateActive, domain.UserStateInitial) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-lgws8wtsqf", "Errors.User.ShouldBeActiveOrInitial")
}
if err := c.checkPermissionUpdateUser(ctx, existingHuman.ResourceOwner, existingHuman.AggregateID); err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, existingHuman, user.NewUserLockedEvent(ctx, &existingHuman.Aggregate().Aggregate)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingHuman.WriteModel), nil
}
func (c *Commands) UnlockUserV2(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-a9ld4xckax", "Errors.User.UserIDMissing")
}
existingHuman, err := c.userStateWriteModel(ctx, userID)
if err != nil {
return nil, err
}
if !isUserStateExists(existingHuman.UserState) {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-x377t913pw", "Errors.User.NotFound")
}
if !hasUserState(existingHuman.UserState, domain.UserStateLocked) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-olb9vb0oca", "Errors.User.NotLocked")
}
if err := c.checkPermissionUpdateUser(ctx, existingHuman.ResourceOwner, existingHuman.AggregateID); err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, existingHuman, user.NewUserUnlockedEvent(ctx, &existingHuman.Aggregate().Aggregate)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingHuman.WriteModel), nil
}
func (c *Commands) DeactivateUserV2(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-78iiirat8y", "Errors.User.UserIDMissing")
}
existingHuman, err := c.userStateWriteModel(ctx, userID)
if err != nil {
return nil, err
}
if !isUserStateExists(existingHuman.UserState) {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-5gp2p62iin", "Errors.User.NotFound")
}
if isUserStateInitial(existingHuman.UserState) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-gvx4kct9r2", "Errors.User.CantDeactivateInitial")
}
if isUserStateInactive(existingHuman.UserState) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5gunjw0cd7", "Errors.User.AlreadyInactive")
}
if err := c.checkPermissionUpdateUser(ctx, existingHuman.ResourceOwner, existingHuman.AggregateID); err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, existingHuman, user.NewUserDeactivatedEvent(ctx, &existingHuman.Aggregate().Aggregate)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingHuman.WriteModel), nil
}
func (c *Commands) ReactivateUserV2(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0nx1ie38fw", "Errors.User.UserIDMissing")
}
existingHuman, err := c.userStateWriteModel(ctx, userID)
if err != nil {
return nil, err
}
if !isUserStateExists(existingHuman.UserState) {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-9hy5kzbuk6", "Errors.User.NotFound")
}
if !isUserStateInactive(existingHuman.UserState) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-s5qqcz97hf", "Errors.User.NotInactive")
}
if err := c.checkPermissionUpdateUser(ctx, existingHuman.ResourceOwner, existingHuman.AggregateID); err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, existingHuman, user.NewUserReactivatedEvent(ctx, &existingHuman.Aggregate().Aggregate)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingHuman.WriteModel), nil
}
func (c *Commands) checkPermissionUpdateUser(ctx context.Context, resourceOwner, userID string) error {
if userID != "" && userID == authz.GetCtxData(ctx).UserID {
return nil
}
if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
return err
}
return nil
}
func (c *Commands) checkPermissionDeleteUser(ctx context.Context, resourceOwner, userID string) error {
if userID != "" && userID == authz.GetCtxData(ctx).UserID {
return nil
}
if err := c.checkPermission(ctx, domain.PermissionUserDelete, resourceOwner, userID); err != nil {
return err
}
return nil
}
func (c *Commands) userStateWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewUserStateWriteModel(userID, "")
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) RemoveUserV2(ctx context.Context, userID string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing")
}
existingUser, err := c.userRemoveWriteModel(ctx, userID)
if err != nil {
return nil, err
}
if !isUserStateExists(existingUser.UserState) {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-bd4ir1mblj", "Errors.User.NotFound")
}
if err := c.checkPermissionDeleteUser(ctx, existingUser.ResourceOwner, existingUser.AggregateID); err != nil {
return nil, err
}
domainPolicy, err := c.domainPolicyWriteModel(ctx, existingUser.ResourceOwner)
if err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-l40ykb3xh2", "Errors.Org.DomainPolicy.NotExisting")
}
var events []eventstore.Command
events = append(events, user.NewUserRemovedEvent(ctx, &existingUser.Aggregate().Aggregate, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain))
for _, grantID := range cascadingGrantIDs {
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true)
if err != nil {
logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant")
continue
}
events = append(events, removeEvent)
}
if len(cascadingUserMemberships) > 0 {
membershipEvents, err := c.removeUserMemberships(ctx, cascadingUserMemberships)
if err != nil {
return nil, err
}
events = append(events, membershipEvents...)
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingUser, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingUser.WriteModel), nil
}
func (c *Commands) userRemoveWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewUserRemoveWriteModel(userID, "")
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}

View File

@@ -67,6 +67,14 @@ func (c *Commands) changeUserEmailWithCode(ctx context.Context, userID, resource
// When the plain text code is returned, no notification e-mail will be send to the user.
// urlTmpl allows changing the target URL that is used by the e-mail and should be a validated Go template, if used.
func (c *Commands) changeUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*domain.Email, error) {
cmd, err := c.changeUserEmailWithGeneratorEvents(ctx, userID, resourceOwner, email, gen, returnCode, urlTmpl)
if err != nil {
return nil, err
}
return cmd.Push(ctx)
}
func (c *Commands) changeUserEmailWithGeneratorEvents(ctx context.Context, userID, resourceOwner, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*UserEmailEvents, error) {
cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner)
if err != nil {
return nil, err
@@ -82,7 +90,7 @@ func (c *Commands) changeUserEmailWithGenerator(ctx context.Context, userID, res
if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil {
return nil, err
}
return cmd.Push(ctx)
return cmd, nil
}
func (c *Commands) VerifyUserEmail(ctx context.Context, userID, resourceOwner, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) {
@@ -167,18 +175,30 @@ func (c *UserEmailEvents) SetVerified(ctx context.Context) {
// AddGeneratedCode generates a new encrypted code and sets it to the email address.
// When returnCode a plain text of the code will be returned from Push.
func (c *UserEmailEvents) AddGeneratedCode(ctx context.Context, gen crypto.Generator, urlTmpl string, returnCode bool) error {
value, plain, err := crypto.NewCode(gen)
cmd, code, err := generateCodeCommand(ctx, c.aggregate, gen, urlTmpl, returnCode)
if err != nil {
return err
}
c.events = append(c.events, user.NewHumanEmailCodeAddedEventV2(ctx, c.aggregate, value, gen.Expiry(), urlTmpl, returnCode))
c.events = append(c.events, cmd)
if returnCode {
c.plainCode = &plain
c.plainCode = &code
}
return nil
}
func generateCodeCommand(ctx context.Context, agg *eventstore.Aggregate, gen crypto.Generator, urlTmpl string, returnCode bool) (eventstore.Command, string, error) {
value, plain, err := crypto.NewCode(gen)
if err != nil {
return nil, "", err
}
cmd := user.NewHumanEmailCodeAddedEventV2(ctx, agg, value, gen.Expiry(), urlTmpl, returnCode)
if returnCode {
return cmd, plain, nil
}
return cmd, "", nil
}
func (c *UserEmailEvents) VerifyCode(ctx context.Context, code string, gen crypto.Generator) error {
if code == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty")

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,558 @@
package command
import (
"context"
"time"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
)
type UserV2WriteModel struct {
eventstore.WriteModel
UserName string
MachineWriteModel bool
Name string
Description string
AccessTokenType domain.OIDCTokenType
MachineSecretWriteModel bool
ClientSecret *crypto.CryptoValue
ProfileWriteModel bool
FirstName string
LastName string
NickName string
DisplayName string
PreferredLanguage language.Tag
Gender domain.Gender
AvatarWriteModel bool
Avatar string
HumanWriteModel bool
InitCode *crypto.CryptoValue
InitCodeCreationDate time.Time
InitCodeExpiry time.Duration
InitCheckFailedCount uint64
PasswordWriteModel bool
PasswordEncodedHash string
PasswordChangeRequired bool
PasswordCode *crypto.CryptoValue
PasswordCodeCreationDate time.Time
PasswordCodeExpiry time.Duration
PasswordCheckFailedCount uint64
EmailWriteModel bool
Email domain.EmailAddress
IsEmailVerified bool
EmailCode *crypto.CryptoValue
EmailCodeCreationDate time.Time
EmailCodeExpiry time.Duration
EmailCheckFailedCount uint64
PhoneWriteModel bool
Phone domain.PhoneNumber
IsPhoneVerified bool
PhoneCode *crypto.CryptoValue
PhoneCodeCreationDate time.Time
PhoneCodeExpiry time.Duration
PhoneCheckFailedCount uint64
StateWriteModel bool
UserState domain.UserState
IDPLinkWriteModel bool
IDPLinks []*domain.UserIDPLink
}
func NewUserExistsWriteModel(userID, resourceOwner string) *UserV2WriteModel {
return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine())
}
func NewUserStateWriteModel(userID, resourceOwner string) *UserV2WriteModel {
return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine(), WithState())
}
func NewUserRemoveWriteModel(userID, resourceOwner string) *UserV2WriteModel {
return newUserV2WriteModel(userID, resourceOwner, WithHuman(), WithMachine(), WithState(), WithIDPLinks())
}
func NewUserHumanWriteModel(userID, resourceOwner string, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinks bool) *UserV2WriteModel {
opts := []UserV2WMOption{WithHuman(), WithState()}
if profileWM {
opts = append(opts, WithProfile())
}
if emailWM {
opts = append(opts, WithEmail())
}
if phoneWM {
opts = append(opts, WithPhone())
}
if passwordWM {
opts = append(opts, WithPassword())
}
if avatarWM {
opts = append(opts, WithAvatar())
}
if idpLinks {
opts = append(opts, WithIDPLinks())
}
return newUserV2WriteModel(userID, resourceOwner, opts...)
}
func newUserV2WriteModel(userID, resourceOwner string, opts ...UserV2WMOption) *UserV2WriteModel {
wm := &UserV2WriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
}
for _, optFunc := range opts {
optFunc(wm)
}
return wm
}
type UserV2WMOption func(o *UserV2WriteModel)
func WithHuman() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.HumanWriteModel = true
}
}
func WithMachine() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.MachineWriteModel = true
}
}
func WithProfile() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.ProfileWriteModel = true
}
}
func WithEmail() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.EmailWriteModel = true
}
}
func WithPhone() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.PhoneWriteModel = true
}
}
func WithPassword() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.PasswordWriteModel = true
}
}
func WithState() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.StateWriteModel = true
}
}
func WithAvatar() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.AvatarWriteModel = true
}
}
func WithIDPLinks() UserV2WMOption {
return func(o *UserV2WriteModel) {
o.IDPLinkWriteModel = true
}
}
func (wm *UserV2WriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanAddedEvent:
wm.reduceHumanAddedEvent(e)
case *user.HumanRegisteredEvent:
wm.reduceHumanRegisteredEvent(e)
case *user.HumanInitialCodeAddedEvent:
wm.UserState = domain.UserStateInitial
wm.SetInitCode(e.Code, e.Expiry, e.CreationDate())
case *user.HumanInitializedCheckSucceededEvent:
wm.UserState = domain.UserStateActive
wm.EmptyInitCode()
case *user.HumanInitializedCheckFailedEvent:
wm.InitCheckFailedCount += 1
case *user.UsernameChangedEvent:
wm.UserName = e.UserName
case *user.HumanProfileChangedEvent:
wm.reduceHumanProfileChangedEvent(e)
case *user.MachineChangedEvent:
if e.Name != nil {
wm.Name = *e.Name
}
if e.Description != nil {
wm.Description = *e.Description
}
if e.AccessTokenType != nil {
wm.AccessTokenType = *e.AccessTokenType
}
case *user.MachineAddedEvent:
wm.UserName = e.UserName
wm.Name = e.Name
wm.Description = e.Description
wm.AccessTokenType = e.AccessTokenType
wm.UserState = domain.UserStateActive
case *user.HumanEmailChangedEvent:
wm.Email = e.EmailAddress
wm.IsEmailVerified = false
wm.EmptyEmailCode()
case *user.HumanEmailCodeAddedEvent:
wm.IsEmailVerified = false
wm.SetEMailCode(e.Code, e.Expiry, e.CreationDate())
case *user.HumanEmailVerifiedEvent:
wm.IsEmailVerified = true
wm.EmptyEmailCode()
case *user.HumanEmailVerificationFailedEvent:
wm.EmailCheckFailedCount += 1
case *user.HumanPhoneChangedEvent:
wm.IsPhoneVerified = false
wm.Phone = e.PhoneNumber
wm.EmptyPhoneCode()
case *user.HumanPhoneCodeAddedEvent:
wm.IsPhoneVerified = false
wm.SetPhoneCode(e.Code, e.Expiry, e.CreationDate())
case *user.HumanPhoneVerifiedEvent:
wm.IsPhoneVerified = true
wm.EmptyPhoneCode()
case *user.HumanPhoneVerificationFailedEvent:
wm.PhoneCheckFailedCount += 1
case *user.HumanPhoneRemovedEvent:
wm.EmptyPhoneCode()
wm.Phone = ""
wm.IsPhoneVerified = false
case *user.HumanAvatarAddedEvent:
wm.Avatar = e.StoreKey
case *user.HumanAvatarRemovedEvent:
wm.Avatar = ""
case *user.UserLockedEvent:
wm.UserState = domain.UserStateLocked
case *user.UserUnlockedEvent:
wm.PasswordCheckFailedCount = 0
wm.UserState = domain.UserStateActive
case *user.UserDeactivatedEvent:
wm.UserState = domain.UserStateInactive
case *user.UserReactivatedEvent:
wm.UserState = domain.UserStateActive
case *user.UserRemovedEvent:
wm.UserState = domain.UserStateDeleted
case *user.HumanPasswordHashUpdatedEvent:
wm.PasswordEncodedHash = e.EncodedHash
case *user.HumanPasswordCheckFailedEvent:
wm.PasswordCheckFailedCount += 1
case *user.HumanPasswordCheckSucceededEvent:
wm.PasswordCheckFailedCount = 0
case *user.HumanPasswordChangedEvent:
wm.PasswordEncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.PasswordChangeRequired = e.ChangeRequired
wm.EmptyPasswordCode()
case *user.HumanPasswordCodeAddedEvent:
wm.SetPasswordCode(e.Code, e.Expiry, e.CreationDate())
case *user.UserIDPLinkAddedEvent:
wm.AddIDPLink(e.IDPConfigID, e.DisplayName, e.ExternalUserID)
case *user.UserIDPLinkRemovedEvent:
wm.RemoveIDPLink(e.IDPConfigID, e.ExternalUserID)
case *user.UserIDPLinkCascadeRemovedEvent:
wm.RemoveIDPLink(e.IDPConfigID, e.ExternalUserID)
}
}
return wm.WriteModel.Reduce()
}
func (wm *UserV2WriteModel) AddIDPLink(configID, displayName, externalUserID string) {
wm.IDPLinks = append(wm.IDPLinks, &domain.UserIDPLink{IDPConfigID: configID, DisplayName: displayName, ExternalUserID: externalUserID})
}
func (wm *UserV2WriteModel) RemoveIDPLink(configID, externalUserID string) {
idx, _ := wm.IDPLinkByID(configID, externalUserID)
if idx < 0 {
return
}
copy(wm.IDPLinks[idx:], wm.IDPLinks[idx+1:])
wm.IDPLinks[len(wm.IDPLinks)-1] = nil
wm.IDPLinks = wm.IDPLinks[:len(wm.IDPLinks)-1]
}
func (wm *UserV2WriteModel) EmptyInitCode() {
wm.InitCode = nil
wm.InitCodeExpiry = 0
wm.InitCodeCreationDate = time.Time{}
wm.InitCheckFailedCount = 0
}
func (wm *UserV2WriteModel) SetInitCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
wm.InitCode = code
wm.InitCodeExpiry = expiry
wm.InitCodeCreationDate = creationDate
wm.InitCheckFailedCount = 0
}
func (wm *UserV2WriteModel) EmptyEmailCode() {
wm.EmailCode = nil
wm.EmailCodeExpiry = 0
wm.EmailCodeCreationDate = time.Time{}
wm.EmailCheckFailedCount = 0
}
func (wm *UserV2WriteModel) SetEMailCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
wm.EmailCode = code
wm.EmailCodeExpiry = expiry
wm.EmailCodeCreationDate = creationDate
wm.EmailCheckFailedCount = 0
}
func (wm *UserV2WriteModel) EmptyPhoneCode() {
wm.PhoneCode = nil
wm.PhoneCodeExpiry = 0
wm.PhoneCodeCreationDate = time.Time{}
wm.PhoneCheckFailedCount = 0
}
func (wm *UserV2WriteModel) SetPhoneCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
wm.PhoneCode = code
wm.PhoneCodeExpiry = expiry
wm.PhoneCodeCreationDate = creationDate
wm.PhoneCheckFailedCount = 0
}
func (wm *UserV2WriteModel) EmptyPasswordCode() {
wm.PasswordCode = nil
wm.PasswordCodeExpiry = 0
wm.PasswordCodeCreationDate = time.Time{}
}
func (wm *UserV2WriteModel) SetPasswordCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
wm.PasswordCode = code
wm.PasswordCodeExpiry = expiry
wm.PasswordCodeCreationDate = creationDate
}
func (wm *UserV2WriteModel) Query() *eventstore.SearchQueryBuilder {
// remove events are always processed
// and username is based for machine and human
eventTypes := []eventstore.EventType{
user.UserRemovedType,
user.UserUserNameChangedType,
}
if wm.HumanWriteModel {
eventTypes = append(eventTypes,
user.UserV1AddedType,
user.HumanAddedType,
user.UserV1RegisteredType,
user.HumanRegisteredType,
)
}
if wm.MachineWriteModel {
eventTypes = append(eventTypes,
user.MachineChangedEventType,
user.MachineAddedEventType,
)
}
if wm.EmailWriteModel {
eventTypes = append(eventTypes,
user.UserV1EmailChangedType,
user.HumanEmailChangedType,
user.UserV1EmailCodeAddedType,
user.HumanEmailCodeAddedType,
user.UserV1EmailVerifiedType,
user.HumanEmailVerifiedType,
user.HumanEmailVerificationFailedType,
user.UserV1EmailVerificationFailedType,
)
}
if wm.PhoneWriteModel {
eventTypes = append(eventTypes,
user.UserV1PhoneChangedType,
user.HumanPhoneChangedType,
user.UserV1PhoneCodeAddedType,
user.HumanPhoneCodeAddedType,
user.UserV1PhoneVerifiedType,
user.HumanPhoneVerifiedType,
user.HumanPhoneVerificationFailedType,
user.UserV1PhoneVerificationFailedType,
user.UserV1PhoneRemovedType,
user.HumanPhoneRemovedType,
)
}
if wm.ProfileWriteModel {
eventTypes = append(eventTypes,
user.UserV1ProfileChangedType,
user.HumanProfileChangedType,
)
}
if wm.StateWriteModel {
eventTypes = append(eventTypes,
user.UserV1InitialCodeAddedType,
user.HumanInitialCodeAddedType,
user.UserV1InitializedCheckSucceededType,
user.HumanInitializedCheckSucceededType,
user.HumanInitializedCheckFailedType,
user.UserV1InitializedCheckFailedType,
user.UserLockedType,
user.UserUnlockedType,
user.UserDeactivatedType,
user.UserReactivatedType,
)
}
if wm.AvatarWriteModel {
eventTypes = append(eventTypes,
user.HumanAvatarAddedType,
user.HumanAvatarRemovedType,
)
}
if wm.PasswordWriteModel {
eventTypes = append(eventTypes,
user.HumanPasswordHashUpdatedType,
user.HumanPasswordChangedType,
user.UserV1PasswordChangedType,
user.HumanPasswordCodeAddedType,
user.UserV1PasswordCodeAddedType,
user.HumanPasswordCheckFailedType,
user.UserV1PasswordCheckFailedType,
user.HumanPasswordCheckSucceededType,
user.UserV1PasswordCheckSucceededType,
)
}
if wm.IDPLinkWriteModel {
eventTypes = append(eventTypes,
user.UserIDPLinkAddedType,
user.UserIDPLinkRemovedType,
user.UserIDPLinkCascadeRemovedType,
)
}
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(eventTypes...).
Builder()
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}
func (wm *UserV2WriteModel) reduceHumanAddedEvent(e *user.HumanAddedEvent) {
wm.UserName = e.UserName
wm.FirstName = e.FirstName
wm.LastName = e.LastName
wm.NickName = e.NickName
wm.DisplayName = e.DisplayName
wm.PreferredLanguage = e.PreferredLanguage
wm.Gender = e.Gender
wm.Email = e.EmailAddress
wm.Phone = e.PhoneNumber
wm.UserState = domain.UserStateActive
wm.PasswordEncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.PasswordChangeRequired = e.ChangeRequired
}
func (wm *UserV2WriteModel) reduceHumanRegisteredEvent(e *user.HumanRegisteredEvent) {
wm.UserName = e.UserName
wm.FirstName = e.FirstName
wm.LastName = e.LastName
wm.NickName = e.NickName
wm.DisplayName = e.DisplayName
wm.PreferredLanguage = e.PreferredLanguage
wm.Gender = e.Gender
wm.Email = e.EmailAddress
wm.Phone = e.PhoneNumber
wm.UserState = domain.UserStateActive
wm.PasswordEncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.PasswordChangeRequired = e.ChangeRequired
}
func (wm *UserV2WriteModel) reduceHumanProfileChangedEvent(e *user.HumanProfileChangedEvent) {
if e.FirstName != "" {
wm.FirstName = e.FirstName
}
if e.LastName != "" {
wm.LastName = e.LastName
}
if e.NickName != nil {
wm.NickName = *e.NickName
}
if e.DisplayName != nil {
wm.DisplayName = *e.DisplayName
}
if e.PreferredLanguage != nil {
wm.PreferredLanguage = *e.PreferredLanguage
}
if e.Gender != nil {
wm.Gender = *e.Gender
}
}
func (wm *UserV2WriteModel) Aggregate() *user.Aggregate {
return user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
}
func (wm *UserV2WriteModel) NewProfileChangedEvent(
ctx context.Context,
firstName,
lastName,
nickName,
displayName *string,
preferredLanguage *language.Tag,
gender *domain.Gender,
) (*user.HumanProfileChangedEvent, error) {
changes := make([]user.ProfileChanges, 0)
if firstName != nil && wm.FirstName != *firstName {
changes = append(changes, user.ChangeFirstName(*firstName))
}
if lastName != nil && wm.LastName != *lastName {
changes = append(changes, user.ChangeLastName(*lastName))
}
if nickName != nil && wm.NickName != *nickName {
changes = append(changes, user.ChangeNickName(*nickName))
}
if displayName != nil && wm.DisplayName != *displayName {
changes = append(changes, user.ChangeDisplayName(*displayName))
}
if preferredLanguage != nil && wm.PreferredLanguage != *preferredLanguage {
changes = append(changes, user.ChangePreferredLanguage(*preferredLanguage))
}
if gender != nil && wm.Gender != *gender {
changes = append(changes, user.ChangeGender(*gender))
}
if len(changes) == 0 {
return nil, nil
}
return user.NewHumanProfileChangedEvent(ctx, &wm.Aggregate().Aggregate, changes)
}
func (wm *UserV2WriteModel) IDPLinkByID(idpID, externalUserID string) (idx int, idp *domain.UserIDPLink) {
for idx, idp = range wm.IDPLinks {
if idp.IDPConfigID == idpID && idp.ExternalUserID == externalUserID {
return idx, idp
}
}
return -1, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
package command
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) changeUsername(ctx context.Context, cmds []eventstore.Command, wm *UserV2WriteModel, userName string) ([]eventstore.Command, error) {
if wm.UserName == userName {
return cmds, nil
}
orgID := wm.ResourceOwner
domainPolicy, err := c.domainPolicyWriteModel(ctx, orgID)
if err != nil {
return cmds, zerrors.ThrowPreconditionFailed(err, "COMMAND-79pv6e1q62", "Errors.Org.DomainPolicy.NotExisting")
}
if !domainPolicy.UserLoginMustBeDomain {
index := strings.LastIndex(userName, "@")
if index > 1 {
domainCheck := NewOrgDomainVerifiedWriteModel(userName[index+1:])
if err := c.eventstore.FilterToQueryReducer(ctx, domainCheck); err != nil {
return cmds, err
}
if domainCheck.Verified && domainCheck.ResourceOwner != orgID {
return cmds, zerrors.ThrowInvalidArgument(nil, "COMMAND-Di2ei", "Errors.User.DomainNotAllowedAsUsername")
}
}
}
return append(cmds,
user.NewUsernameChangedEvent(ctx, &wm.Aggregate().Aggregate, wm.UserName, userName, domainPolicy.UserLoginMustBeDomain),
), nil
}

View File

@@ -29,6 +29,7 @@ type PermissionCheck func(ctx context.Context, permission, orgID, resourceID str
const (
PermissionUserWrite = "user.write"
PermissionUserRead = "user.read"
PermissionUserDelete = "user.delete"
PermissionSessionWrite = "session.write"
PermissionSessionDelete = "session.delete"
)

View File

@@ -19,6 +19,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/idp/providers/saml"
@@ -31,6 +32,7 @@ import (
organisation "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/system"
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
@@ -134,6 +136,17 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
return resp
}
func (s *Tester) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResponse {
resp, err := s.Client.Mgmt.AddMachineUser(ctx, &mgmt.AddMachineUserRequest{
UserName: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Name: "Mickey",
Description: "Mickey Mouse",
AccessTokenType: user_pb.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER,
})
logging.OnError(err).Fatal("create human user")
return resp
}
func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user.AddIDPLinkResponse {
resp, err := s.Client.UserV2.AddIDPLink(
ctx,
@@ -406,3 +419,29 @@ func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID
return createResp.GetSessionId(), createResp.GetSessionToken(),
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
}
func (s *Tester) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string {
resp, err := s.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{
UserId: userID,
ProjectId: projectID,
})
require.NoError(t, err)
return resp.GetUserGrantId()
}
func (s *Tester) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) {
_, err := s.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{
UserId: userID,
Roles: []string{domain.RoleOrgOwner},
})
require.NoError(t, err)
}
func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) {
_, err := s.Client.Mgmt.AddProjectMember(ctx, &mgmt.AddProjectMemberRequest{
ProjectId: projectID,
UserId: userID,
Roles: []string{domain.RoleProjectOwner},
})
require.NoError(t, err)
}