diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index a8a0a374cc..545d4ff02d 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -298,7 +298,18 @@ An sms will be sent to the given phone number to finish the phone verification p > **rpc** SetHumanInitialPassword([SetHumanInitialPasswordRequest](#sethumaninitialpasswordrequest)) [SetHumanInitialPasswordResponse](#sethumaninitialpasswordresponse) -A Manager is only allowed to set an initial password, on the next login the user has to change his password +deprecated: use SetHumanPassword + + + + +### SetHumanPassword + +> **rpc** SetHumanPassword([SetHumanPasswordRequest](#sethumanpasswordrequest)) +[SetHumanPasswordResponse](#sethumanpasswordresponse) + +Set a new password for a user, on default the user has to change the password on the next login +Set no_change_required to true if the user does not have to change the password on the next login @@ -4862,6 +4873,30 @@ This is an empty request +### SetHumanPasswordRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_id | string | - | string.min_len: 1
| +| password | string | - | string.min_len: 1
string.max_len: 72
| +| no_change_required | bool | - | | + + + + +### SetHumanPasswordResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetPrimaryOrgDomainRequest diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 6db886f4e9..7189e32912 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -324,7 +324,7 @@ func (s *Server) ResendHumanPhoneVerification(ctx context.Context, req *mgmt_pb. } func (s *Server) SetHumanInitialPassword(ctx context.Context, req *mgmt_pb.SetHumanInitialPasswordRequest) (*mgmt_pb.SetHumanInitialPasswordResponse, error) { - objectDetails, err := s.command.SetOneTimePassword(ctx, authz.GetCtxData(ctx).OrgID, req.UserId, req.Password) + objectDetails, err := s.command.SetPassword(ctx, authz.GetCtxData(ctx).OrgID, req.UserId, req.Password, true) if err != nil { return nil, err } @@ -333,6 +333,16 @@ func (s *Server) SetHumanInitialPassword(ctx context.Context, req *mgmt_pb.SetHu }, nil } +func (s *Server) SetHumanPassword(ctx context.Context, req *mgmt_pb.SetHumanPasswordRequest) (*mgmt_pb.SetHumanPasswordResponse, error) { + objectDetails, err := s.command.SetPassword(ctx, authz.GetCtxData(ctx).OrgID, req.UserId, req.Password, !req.NoChangeRequired) + if err != nil { + return nil, err + } + return &mgmt_pb.SetHumanPasswordResponse{ + Details: obj_grpc.DomainToChangeDetailsPb(objectDetails), + }, nil +} + func (s *Server) SendHumanResetPasswordNotification(ctx context.Context, req *mgmt_pb.SendHumanResetPasswordNotificationRequest) (*mgmt_pb.SendHumanResetPasswordNotificationResponse, error) { objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type)) if err != nil { diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go index 3572650c35..98ee4fa6c3 100644 --- a/internal/command/user_human_password.go +++ b/internal/command/user_human_password.go @@ -11,7 +11,7 @@ import ( "github.com/caos/zitadel/internal/telemetry/tracing" ) -func (c *Commands) SetOneTimePassword(ctx context.Context, orgID, userID, passwordString string) (objectDetails *domain.ObjectDetails, err error) { +func (c *Commands) SetPassword(ctx context.Context, orgID, userID, passwordString string, oneTime bool) (objectDetails *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if userID == "" { @@ -26,7 +26,7 @@ func (c *Commands) SetOneTimePassword(ctx context.Context, orgID, userID, passwo } password := &domain.Password{ SecretString: passwordString, - ChangeRequired: true, + ChangeRequired: oneTime, } userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel) passwordEvent, err := c.changePassword(ctx, "", password, userAgg, existingPassword) @@ -44,7 +44,7 @@ func (c *Commands) SetOneTimePassword(ctx context.Context, orgID, userID, passwo return writeModelToObjectDetails(&existingPassword.WriteModel), nil } -func (c *Commands) SetPassword(ctx context.Context, orgID, userID, code, passwordString, userAgentID string) (err error) { +func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, passwordString, userAgentID string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go index 1526b626bb..c7ee208f0c 100644 --- a/internal/command/user_human_password_test.go +++ b/internal/command/user_human_password_test.go @@ -28,6 +28,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) { userID string resourceOwner string password string + oneTime bool } type res struct { want *domain.ObjectDetails @@ -72,7 +73,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) { }, }, { - name: "change password, ok", + name: "change password onetime, ok", fields: fields{ eventstore: eventstoreExpect( t, @@ -134,6 +135,78 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) { userID: "user1", resourceOwner: "org1", password: "password", + oneTime: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "change password no one time, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password"), + }, + false, + "", + ), + ), + }, + ), + ), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + oneTime: false, }, res: res{ want: &domain.ObjectDetails{ @@ -148,7 +221,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) { eventstore: tt.fields.eventstore, userPasswordAlg: tt.fields.userPasswordAlg, } - got, err := r.SetOneTimePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password) + got, err := r.SetPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.oneTime) if tt.res.err == nil { assert.NoError(t, err) } @@ -410,7 +483,7 @@ func TestCommandSide_SetPassword(t *testing.T) { userPasswordAlg: tt.fields.userPasswordAlg, passwordVerificationCode: tt.fields.secretGenerator, } - err := r.SetPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID) + err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/ui/login/handler/init_password_handler.go b/internal/ui/login/handler/init_password_handler.go index ef57df9423..882f145334 100644 --- a/internal/ui/login/handler/init_password_handler.go +++ b/internal/ui/login/handler/init_password_handler.go @@ -69,7 +69,7 @@ func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *dom userOrg = authReq.UserOrgID } userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - err = l.command.SetPassword(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, userAgentID) if err != nil { l.renderInitPassword(w, r, authReq, data.UserID, "", err) return diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 906bdeb292..a8da4d17da 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -392,7 +392,7 @@ service ManagementService { }; } - // A Manager is only allowed to set an initial password, on the next login the user has to change his password + // deprecated: use SetHumanPassword rpc SetHumanInitialPassword(SetHumanInitialPasswordRequest) returns (SetHumanInitialPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_initialize" @@ -404,6 +404,19 @@ service ManagementService { }; } + // Set a new password for a user, on default the user has to change the password on the next login + // Set no_change_required to true if the user does not have to change the password on the next login + rpc SetHumanPassword(SetHumanPasswordRequest) returns (SetHumanPasswordResponse) { + option (google.api.http) = { + post: "/users/{user_id}/password" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "user.write" + }; + } + // An email will be sent to the given address to reset the password of the user rpc SendHumanResetPasswordNotification(SendHumanResetPasswordNotificationRequest) returns (SendHumanResetPasswordNotificationResponse) { option (google.api.http) = { @@ -2380,6 +2393,16 @@ message SetHumanInitialPasswordResponse { zitadel.v1.ObjectDetails details = 1; } +message SetHumanPasswordRequest { + string user_id = 1 [(validate.rules).string.min_len = 1]; + string password = 2 [(validate.rules).string = {min_len: 1, max_len: 72}]; + bool no_change_required = 3; +} + +message SetHumanPasswordResponse { + zitadel.v1.ObjectDetails details = 1; +} + message SendHumanResetPasswordNotificationRequest { enum Type { TYPE_EMAIL = 0;