Merge branch 'main' into next-rc

This commit is contained in:
Livio Spring 2023-12-21 10:05:47 +01:00
commit 48addb2464
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
35 changed files with 9369 additions and 99 deletions

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -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},

View File

@ -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\"";
}
];
}
}

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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},