mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-14 03:54:21 +00:00
Merge branch 'main' into next-rc
This commit is contained in:
commit
48addb2464
@ -27,7 +27,7 @@ You will benefit from the transparency of the open source and the hyper-scalabil
|
||||
|
||||
#### Benefits over using open source / community license
|
||||
|
||||
- [Enterprise supported features](support-services) are only supported under an Enterprise license
|
||||
- [Enterprise supported features](/docs/support/software-release-cycles-support#enterprise-supported) are only supported under an Enterprise license
|
||||
- Individual [onboarding support](./support-services#onboarding-support) tailored to your needs and team
|
||||
- Get access to our support with a [Service Level Agreement](support-services#service-level-agreement) that is tailored to your needs
|
||||
- Benefit from personal [technical account management](support-services#technical-account-manager) provided by our engineers to help you with architecture, integration, migration, and operational improvements of your setup
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) }()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) }()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
213
internal/command/user_v2.go
Normal 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
|
||||
}
|
@ -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")
|
||||
|
457
internal/command/user_v2_human.go
Normal file
457
internal/command/user_v2_human.go
Normal 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
|
||||
}
|
2568
internal/command/user_v2_human_test.go
Normal file
2568
internal/command/user_v2_human_test.go
Normal file
File diff suppressed because it is too large
Load Diff
558
internal/command/user_v2_model.go
Normal file
558
internal/command/user_v2_model.go
Normal 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
|
||||
}
|
2386
internal/command/user_v2_model_test.go
Normal file
2386
internal/command/user_v2_model_test.go
Normal file
File diff suppressed because it is too large
Load Diff
1413
internal/command/user_v2_test.go
Normal file
1413
internal/command/user_v2_test.go
Normal file
File diff suppressed because it is too large
Load Diff
37
internal/command/user_v2_username.go
Normal file
37
internal/command/user_v2_username.go
Normal 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
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -27,6 +27,18 @@ message SetHumanEmail {
|
||||
}
|
||||
}
|
||||
|
||||
message HumanEmail {
|
||||
string email = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"mini@mouse.com\"";
|
||||
}
|
||||
];
|
||||
bool is_verified = 2;
|
||||
}
|
||||
|
||||
|
||||
message SendEmailVerificationCode {
|
||||
optional string url_template = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
|
@ -55,3 +55,31 @@ enum NotificationType {
|
||||
NOTIFICATION_TYPE_Email = 1;
|
||||
NOTIFICATION_TYPE_SMS = 2;
|
||||
}
|
||||
|
||||
message SetPassword {
|
||||
oneof password_type {
|
||||
Password password = 1;
|
||||
HashedPassword hashed_password = 2;
|
||||
}
|
||||
oneof verification {
|
||||
string current_password = 3 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"Secr3tP4ssw0rd!\"";
|
||||
}
|
||||
];
|
||||
string verification_code = 4 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 20},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 20;
|
||||
example: "\"SKJd342k\"";
|
||||
description: "\"the verification code generated during password reset request\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
@ -24,6 +24,16 @@ message SetHumanPhone {
|
||||
}
|
||||
}
|
||||
|
||||
message HumanPhone {
|
||||
string phone = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
max_length: 200;
|
||||
example: "\"+41791234567\"";
|
||||
}
|
||||
];
|
||||
bool is_verified = 2;
|
||||
}
|
||||
|
||||
message SendPhoneVerificationCode {}
|
||||
|
||||
message ReturnPhoneVerificationCode {}
|
||||
|
@ -7,10 +7,9 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2beta;user";
|
||||
import "google/api/field_behavior.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
message User {
|
||||
string id = 1;
|
||||
}
|
||||
import "zitadel/object/v2beta/object.proto";
|
||||
import "zitadel/user/v2beta/email.proto";
|
||||
import "zitadel/user/v2beta/phone.proto";
|
||||
|
||||
enum Gender {
|
||||
GENDER_UNSPECIFIED = 0;
|
||||
@ -66,6 +65,45 @@ message SetHumanProfile {
|
||||
];
|
||||
}
|
||||
|
||||
message HumanProfile {
|
||||
string given_name = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"Minnie\"";
|
||||
}
|
||||
];
|
||||
string family_name = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"Mouse\"";
|
||||
}
|
||||
];
|
||||
optional string nick_name = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
max_length: 200;
|
||||
example: "\"Mini\"";
|
||||
}
|
||||
];
|
||||
optional string display_name = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
max_length: 200;
|
||||
example: "\"Minnie Mouse\"";
|
||||
}
|
||||
];
|
||||
optional string preferred_language = 5 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
max_length: 10;
|
||||
example: "\"en\"";
|
||||
}
|
||||
];
|
||||
optional zitadel.user.v2beta.Gender gender = 6 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"GENDER_FEMALE\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetMetadataEntry {
|
||||
string key = 1 [
|
||||
@ -88,3 +126,44 @@ message SetMetadataEntry {
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message HumanUser {
|
||||
string user_id = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
|
||||
}
|
||||
];
|
||||
UserState state = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "current state of the user";
|
||||
}
|
||||
];
|
||||
string username = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"minnie-mouse\"";
|
||||
}
|
||||
];
|
||||
repeated string login_names = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "[\"gigi@zitadel.com\", \"gigi@zitadel.zitadel.ch\"]";
|
||||
}
|
||||
];
|
||||
string preferred_login_name = 5 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"gigi@zitadel.com\"";
|
||||
}
|
||||
];
|
||||
HumanProfile profile = 6;
|
||||
HumanEmail email = 7;
|
||||
HumanPhone phone = 8;
|
||||
}
|
||||
|
||||
enum UserState {
|
||||
USER_STATE_UNSPECIFIED = 0;
|
||||
USER_STATE_ACTIVE = 1;
|
||||
USER_STATE_INACTIVE = 2;
|
||||
USER_STATE_DELETED = 3;
|
||||
USER_STATE_LOCKED = 4;
|
||||
USER_STATE_SUSPEND = 5;
|
||||
USER_STATE_INITIAL = 6;
|
||||
}
|
@ -238,6 +238,149 @@ service UserService {
|
||||
};
|
||||
}
|
||||
|
||||
rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/v2beta/users/{user_id}"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Update User";
|
||||
description: "Update all information from a user."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc DeactivateUser(DeactivateUserRequest) returns (DeactivateUserResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2beta/users/{user_id}/deactivate"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Deactivate user";
|
||||
description: "The state of the user will be changed to 'deactivated'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'deactivated'. Use deactivate user when the user should not be able to use the account anymore, but you still need access to the user data."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc ReactivateUser(ReactivateUserRequest) returns (ReactivateUserResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2beta/users/{user_id}/reactivate"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Reactivate user";
|
||||
description: "Reactivate a user with the state 'deactivated'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'deactivated'."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc LockUser(LockUserRequest) returns (LockUserResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2beta/users/{user_id}/lock"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Lock user";
|
||||
description: "The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.)"
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc UnlockUser(UnlockUserRequest) returns (UnlockUserResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2beta/users/{user_id}/unlock"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Unlock user";
|
||||
description: "Unlock a user with the state 'locked'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'locked'."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v2beta/users/{user_id}"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "user.delete"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Delete user";
|
||||
description: "The state of the user will be changed to 'deleted'. The user will not be able to log in anymore. Endpoints requesting this user will return an error 'User not found"
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc RegisterPasskey (RegisterPasskeyRequest) returns (RegisterPasskeyResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2beta/users/{user_id}/passkeys"
|
||||
@ -804,6 +947,133 @@ message VerifyPhoneResponse{
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message DeleteUserRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629012906488334\"";
|
||||
}];
|
||||
}
|
||||
|
||||
message DeleteUserResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message GetUserByIDRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629012906488334\"";
|
||||
description: "User ID of the user you like to get."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message GetUserByIDResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
HumanUser user = 2;
|
||||
}
|
||||
|
||||
message UpdateHumanUserRequest{
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
|
||||
}
|
||||
];
|
||||
optional string username = 2 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"minnie-mouse\"";
|
||||
}
|
||||
];
|
||||
optional SetHumanProfile profile = 3;
|
||||
optional SetHumanEmail email = 4;
|
||||
optional SetHumanPhone phone = 5;
|
||||
optional SetPassword password = 6;
|
||||
}
|
||||
|
||||
message UpdateHumanUserResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
optional string email_code = 2;
|
||||
optional string phone_code = 3;
|
||||
}
|
||||
|
||||
message DeactivateUserRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629012906488334\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message DeactivateUserResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
|
||||
message ReactivateUserRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629012906488334\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message ReactivateUserResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message LockUserRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629012906488334\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message LockUserResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message UnlockUserRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"69629012906488334\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message UnlockUserResponse {
|
||||
zitadel.object.v2beta.Details details = 1;
|
||||
}
|
||||
|
||||
message RegisterPasskeyRequest{
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
|
Loading…
Reference in New Issue
Block a user