From 1e9d58c924543966748e01a4512d8e5034b17ceb Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:05:04 +0200 Subject: [PATCH] feat: SetPassword endpoint --- .../v3alpha/integration_test/password_test.go | 234 ++++++++++ .../grpc/resources/user/v3alpha/password.go | 33 ++ internal/command/user_v3_password.go | 123 ++++++ internal/command/user_v3_password_model.go | 53 +++ internal/command/user_v3_password_test.go | 416 ++++++++++++++++++ internal/command/user_v3_username.go | 28 ++ internal/command/user_v3_username_test.go | 2 +- 7 files changed, 888 insertions(+), 1 deletion(-) create mode 100644 internal/api/grpc/resources/user/v3alpha/integration_test/password_test.go create mode 100644 internal/api/grpc/resources/user/v3alpha/password.go create mode 100644 internal/command/user_v3_password.go create mode 100644 internal/command/user_v3_password_model.go create mode 100644 internal/command/user_v3_password_test.go diff --git a/internal/api/grpc/resources/user/v3alpha/integration_test/password_test.go b/internal/api/grpc/resources/user/v3alpha/integration_test/password_test.go new file mode 100644 index 0000000000..35557cf959 --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/password_test.go @@ -0,0 +1,234 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +func TestServer_SetPassword(t *testing.T) { + t.Parallel() + instance := integration.NewInstance(CTX) + ensureFeatureEnabled(t, instance) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + schema := []byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`) + schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema) + orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) + + type res struct { + want *resource_object.Details + } + tests := []struct { + name string + ctx context.Context + dep func(req *user.SetPasswordRequest) error + req *user.SetPasswordRequest + res res + wantErr bool + }{ + { + name: "password set, no context", + ctx: context.Background(), + dep: func(req *user.SetPasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: gofakeit.Password(true, true, true, true, false, 12), + }, + ChangeRequired: false, + }, + }, + wantErr: true, + }, + { + name: "password set, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.SetPasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: gofakeit.Password(true, true, true, true, false, 12), + }, + ChangeRequired: false, + }, + }, + wantErr: true, + }, + { + name: "password set, password empty", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.SetPasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: "", + }, + ChangeRequired: false, + }, + }, + wantErr: true, + }, + { + name: "password set, user not existing in org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.SetPasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: gofakeit.Password(true, true, true, true, false, 12), + }, + ChangeRequired: false, + }, + }, + wantErr: true, + }, + { + name: "username add, user not existing", + ctx: isolatedIAMOwnerCTX, + + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + Id: "not existing", + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: gofakeit.Password(true, true, true, true, false, 12), + }, + ChangeRequired: false, + }, + }, + wantErr: true, + }, + { + name: "password set, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.SetPasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: gofakeit.Password(true, true, true, true, false, 12), + }, + ChangeRequired: false, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "password set, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.SetPasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.SetPasswordRequest{ + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: gofakeit.Password(true, true, true, true, false, 12), + }, + ChangeRequired: false, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + err := tt.dep(tt.req) + assert.NoError(t, err) + } + got, err := instance.Client.UserV3Alpha.SetPassword(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.res.want, got.Details) + }) + } +} diff --git a/internal/api/grpc/resources/user/v3alpha/password.go b/internal/api/grpc/resources/user/v3alpha/password.go new file mode 100644 index 0000000000..5559bf57bf --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/password.go @@ -0,0 +1,33 @@ +package user + +import ( + "context" + + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/command" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + details, err := s.command.SetSchemaUserPassword(ctx, setPasswordRequestToSetSchemaUserPassword(req)) + if err != nil { + return nil, err + } + return &user.SetPasswordResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + }, nil +} + +func setPasswordRequestToSetSchemaUserPassword(req *user.SetPasswordRequest) *command.SetSchemaUserPassword { + return &command.SetSchemaUserPassword{ + ResourceOwner: organizationToUpdateResourceOwner(req.Organization), + UserID: req.GetId(), + Password: req.GetNewPassword().GetPassword(), + EncodedPasswordHash: req.GetNewPassword().GetHash(), + ChangeRequired: req.GetNewPassword().GetChangeRequired(), + } +} diff --git a/internal/command/user_v3_password.go b/internal/command/user_v3_password.go new file mode 100644 index 0000000000..33dee929c2 --- /dev/null +++ b/internal/command/user_v3_password.go @@ -0,0 +1,123 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/repository/user/authenticator" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type SetSchemaUserPassword struct { + ResourceOwner string + UserID string + + Password string + EncodedPasswordHash string + ChangeRequired bool +} + +func (p *SetSchemaUserPassword) Validate(hasher *crypto.Hasher) (err error) { + if p.UserID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing") + } + + if p.EncodedPasswordHash != "" { + if !hasher.EncodingSupported(p.EncodedPasswordHash) { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-oz74onzvqr", "Errors.User.Password.NotSupported") + } + } + if p.Password == "" && p.EncodedPasswordHash == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-3klek4sbns", "Errors.User.Password.Empty") + } + + return nil +} + +func (c *Commands) SetSchemaUserPassword(ctx context.Context, username *SetSchemaUserPassword) (*domain.ObjectDetails, error) { + if err := username.Validate(c.userPasswordHasher); err != nil { + return nil, err + } + + existing, err := c.getPasswordExistsWithVerification(ctx, username.ResourceOwner, username.UserID) + if err != nil { + return nil, err + } + resourceOwner := existing.ResourceOwner + if existing.EncodedHash == "" { + existingUser, err := c.getSchemaUserExists(ctx, username.ResourceOwner, username.UserID) + if err != nil { + return nil, err + } + if !existingUser.Exists() { + return nil, zerrors.ThrowNotFound(nil, "TODO", "TODO") + } + resourceOwner = existingUser.ResourceOwner + } + + // If password is provided, let's check if is compliant with the policy. + // If only a encodedPassword is passed, we can skip this. + if username.Password != "" { + if err = c.checkPasswordComplexity(ctx, username.Password, resourceOwner); err != nil { + return nil, err + } + } + + encodedPassword := username.EncodedPasswordHash + if username.Password != "" { + encodedPassword, err = c.userPasswordHasher.Hash(username.Password) + if err = convertPasswapErr(err); err != nil { + return nil, err + } + } + + events, err := c.eventstore.Push(ctx, + authenticator.NewPasswordCreatedEvent(ctx, + &authenticator.NewAggregate(username.UserID, resourceOwner).Aggregate, + existing.UserID, + encodedPassword, + username.ChangeRequired, + ), + ) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(events), nil +} + +func (c *Commands) DeleteSchemaUserPassword(ctx context.Context, resourceOwner, id string) (_ *domain.ObjectDetails, err error) { + existing, err := c.getPasswordExistsWithVerification(ctx, resourceOwner, id) + if err != nil { + return nil, err + } + if existing.EncodedHash == "" { + return nil, zerrors.ThrowNotFound(nil, "TODO", "TODO") + } + + events, err := c.eventstore.Push(ctx, + authenticator.NewPasswordDeletedEvent(ctx, + &authenticator.NewAggregate(id, existing.ResourceOwner).Aggregate, + ), + ) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(events), nil +} + +func (c *Commands) getPasswordExistsWithVerification(ctx context.Context, resourceOwner, id string) (*PasswordV3WriteModel, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-PoSU5BOZCi", "Errors.IDMissing") + } + writeModel := NewPasswordV3WriteModel(resourceOwner, id) + if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + return nil, err + } + + // TODO permission through old password and password code + if err := c.checkPermissionUpdateUser(ctx, writeModel.ResourceOwner, writeModel.UserID); err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/user_v3_password_model.go b/internal/command/user_v3_password_model.go new file mode 100644 index 0000000000..4295d1fc3a --- /dev/null +++ b/internal/command/user_v3_password_model.go @@ -0,0 +1,53 @@ +package command + +import ( + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user/authenticator" + "github.com/zitadel/zitadel/internal/repository/user/schemauser" +) + +type PasswordV3WriteModel struct { + eventstore.WriteModel + UserID string + + EncodedHash string + ChangeRequired bool +} + +func NewPasswordV3WriteModel(resourceOwner, id string) *PasswordV3WriteModel { + return &PasswordV3WriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: id, + ResourceOwner: resourceOwner, + }, + UserID: id, + } +} + +func (wm *PasswordV3WriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *authenticator.PasswordCreatedEvent: + wm.UserID = e.UserID + wm.EncodedHash = e.EncodedHash + wm.ChangeRequired = e.ChangeRequired + case *authenticator.PasswordDeletedEvent: + wm.UserID = "" + wm.EncodedHash = "" + wm.ChangeRequired = false + } + } + return wm.WriteModel.Reduce() +} + +func (wm *PasswordV3WriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(schemauser.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + authenticator.PasswordCreatedType, + authenticator.PasswordDeletedType, + ).Builder() +} diff --git a/internal/command/user_v3_password_test.go b/internal/command/user_v3_password_test.go new file mode 100644 index 0000000000..6ce5520930 --- /dev/null +++ b/internal/command/user_v3_password_test.go @@ -0,0 +1,416 @@ +package command + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/user/authenticator" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func filterSchemaUserPasswordExisting() expect { + return expectFilter( + eventFromEventPusher( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "user1", + "encoded", + false, + ), + ), + ) +} + +func filterPasswordComplexityPolicyExisting() expect { + return expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ) +} + +func TestCommands_SetSchemaUserPassword(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + userPasswordHasher *crypto.Hasher + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + user *SetSchemaUserPassword + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no userID, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing")) + }, + }, + }, + { + "no password, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-3klek4sbns", "Errors.User.Password.Empty")) + }, + }, + }, + { + "user not existing, error", + fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "notexisting", + Password: "password", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "TODO", "TODO")) + }, + }, + }, + { + "no permission, error", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "password", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + "password added, ok", + fields{ + eventstore: expectEventstore( + expectFilter(), + filterSchemaUserExisting(), + filterPasswordComplexityPolicyExisting(), + expectPush( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "user1", + "$plain$x$password", + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "password", + ChangeRequired: false, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "password set, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + filterPasswordComplexityPolicyExisting(), + expectPush( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "user1", + "$plain$x$password", + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "password", + ChangeRequired: false, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "password set, changeRequired, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + filterPasswordComplexityPolicyExisting(), + expectPush( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "user1", + "$plain$x$password", + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "password", + ChangeRequired: true, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + userPasswordHasher: tt.fields.userPasswordHasher, + } + details, err := c.SetSchemaUserPassword(tt.args.ctx, tt.args.user) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.details, details) + } + }) + } +} + +func TestCommands_DeleteSchemaUserPassword(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + resourceOwner string + id string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no ID, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "", + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-PoSU5BOZCi", "Errors.IDMissing")) + }, + }, + }, + { + "password not existing, error", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "notexisting", + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "TODO", "TODO")) + }, + }, + }, + { + "password already removed, error", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "id1", + "hash", + false, + ), + ), + eventFromEventPusher( + authenticator.NewPasswordDeletedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "user1", + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "TODO", "TODO")) + }, + }, + }, + { + "no permission, error", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "user1", + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + "password removed, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + expectPush( + authenticator.NewPasswordDeletedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "user1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + details, err := c.DeleteSchemaUserPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.id) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.details, details) + } + }) + } +} diff --git a/internal/command/user_v3_username.go b/internal/command/user_v3_username.go index e072ef9247..ea2f1e0f87 100644 --- a/internal/command/user_v3_username.go +++ b/internal/command/user_v3_username.go @@ -75,6 +75,34 @@ func (c *Commands) getSchemaUsernameExistsWithPermission(ctx context.Context, re return writeModel, nil } +func existingSchemaUser(ctx context.Context, c *Commands, resourceOwner, userID string) (*UserV3WriteModel, error) { + if userID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing") + } + existingUser, err := c.getSchemaUserExists(ctx, resourceOwner, userID) + if err != nil { + return nil, err + } + if !existingUser.Exists() { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-6T2xrOHxTx", "Errors.User.NotFound") + } + + if err := c.checkPermissionUpdateUser(ctx, existingUser.ResourceOwner, existingUser.AggregateID); err != nil { + return nil, err + } + + existingSchema, err := c.getSchemaWriteModelByID(ctx, "", existingUser.SchemaID) + if err != nil { + return nil, err + } + if !existingSchema.Exists() { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-6T2xrOHxTx", "TODO") + } + + //TODO possible authenticators check + return existingUser, nil +} + func existingSchemaUserWithPermission(ctx context.Context, c *Commands, resourceOwner, userID string) (*UserV3WriteModel, error) { if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing") diff --git a/internal/command/user_v3_username_test.go b/internal/command/user_v3_username_test.go index f581577a93..f5ad96e124 100644 --- a/internal/command/user_v3_username_test.go +++ b/internal/command/user_v3_username_test.go @@ -391,7 +391,7 @@ func TestCommands_DeleteUsername(t *testing.T) { }, }, { - "username added, isOrgSpecific, ok", + "username removed, isOrgSpecific, ok", fields{ eventstore: expectEventstore( filterUsernameExisting(true),