From 50e0c758dea88632da799655b44b5137ff1d3c0d Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Tue, 24 Sep 2024 20:42:20 +0200 Subject: [PATCH] feat: ResetPassword endpoint --- .../v3alpha/integration_test/password_test.go | 607 +++++++++++++++++- .../v3alpha/integration_test/user_test.go | 37 +- .../v3alpha/integration_test/username_test.go | 61 +- .../grpc/resources/user/v3alpha/password.go | 52 +- .../grpc/resources/user/v3alpha/username.go | 4 +- internal/command/debug_events_test.go | 4 + internal/command/instance_features_test.go | 8 + internal/command/system_features_test.go | 7 + internal/command/user_v3_password.go | 3 +- internal/command/user_v3_password_model.go | 6 +- internal/command/user_v3_password_test.go | 457 ++++++++++++- internal/command/user_v3_username.go | 11 +- internal/command/user_v3_username_model.go | 11 +- internal/command/user_v3_username_test.go | 51 +- internal/integration/client.go | 34 +- .../user/authenticator/eventstore.go | 1 + .../resources/user/v3alpha/user_service.proto | 10 + 17 files changed, 1275 insertions(+), 89 deletions(-) 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 index 35557cf959..8e07c91658 100644 --- a/internal/api/grpc/resources/user/v3alpha/integration_test/password_test.go +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/password_test.go @@ -62,7 +62,7 @@ func TestServer_SetPassword(t *testing.T) { }, NewPassword: &user.SetPassword{ Type: &user.SetPassword_Password{ - Password: gofakeit.Password(true, true, true, true, false, 12), + Password: fakePassword(), }, ChangeRequired: false, }, @@ -85,7 +85,7 @@ func TestServer_SetPassword(t *testing.T) { }, NewPassword: &user.SetPassword{ Type: &user.SetPassword_Password{ - Password: gofakeit.Password(true, true, true, true, false, 12), + Password: fakePassword(), }, ChangeRequired: false, }, @@ -131,7 +131,7 @@ func TestServer_SetPassword(t *testing.T) { }, NewPassword: &user.SetPassword{ Type: &user.SetPassword_Password{ - Password: gofakeit.Password(true, true, true, true, false, 12), + Password: fakePassword(), }, ChangeRequired: false, }, @@ -139,7 +139,7 @@ func TestServer_SetPassword(t *testing.T) { wantErr: true, }, { - name: "username add, user not existing", + name: "password set, user not existing", ctx: isolatedIAMOwnerCTX, req: &user.SetPasswordRequest{ @@ -151,7 +151,7 @@ func TestServer_SetPassword(t *testing.T) { Id: "not existing", NewPassword: &user.SetPassword{ Type: &user.SetPassword_Password{ - Password: gofakeit.Password(true, true, true, true, false, 12), + Password: fakePassword(), }, ChangeRequired: false, }, @@ -174,7 +174,7 @@ func TestServer_SetPassword(t *testing.T) { }, NewPassword: &user.SetPassword{ Type: &user.SetPassword_Password{ - Password: gofakeit.Password(true, true, true, true, false, 12), + Password: fakePassword(), }, ChangeRequired: false, }, @@ -200,7 +200,7 @@ func TestServer_SetPassword(t *testing.T) { req: &user.SetPasswordRequest{ NewPassword: &user.SetPassword{ Type: &user.SetPassword_Password{ - Password: gofakeit.Password(true, true, true, true, false, 12), + Password: fakePassword(), }, ChangeRequired: false, }, @@ -215,6 +215,174 @@ func TestServer_SetPassword(t *testing.T) { }, }, }, + { + name: "password set, code, 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() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + passwordResp := instance.RequestAuthenticatorPasswordReset(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + req.NewPassword.Verification = &user.SetPassword_VerificationCode{VerificationCode: passwordResp.GetVerificationCode()} + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: fakePassword(), + }, + 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, code, failed", + 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() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + instance.RequestAuthenticatorPasswordReset(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id) + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: fakePassword(), + }, + ChangeRequired: false, + Verification: &user.SetPassword_VerificationCode{VerificationCode: "notreally"}, + }, + }, + wantErr: true, + }, + { + name: "password set, code, no set", + 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() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: fakePassword(), + }, + ChangeRequired: false, + Verification: &user.SetPassword_VerificationCode{VerificationCode: "notreally"}, + }, + }, + wantErr: true, + }, + { + name: "password set, current password, 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() + password := fakePassword() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, password) + req.NewPassword.Verification = &user.SetPassword_CurrentPassword{CurrentPassword: password} + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: fakePassword(), + }, + 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, current password, failed", + 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() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.SetPasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + NewPassword: &user.SetPassword{ + Type: &user.SetPassword_Password{ + Password: fakePassword(), + }, + ChangeRequired: false, + Verification: &user.SetPassword_CurrentPassword{CurrentPassword: fakePassword()}, + }, + }, + wantErr: true, + }, + { + name: "password set, current password, not set", + 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: fakePassword(), + }, + ChangeRequired: false, + Verification: &user.SetPassword_CurrentPassword{CurrentPassword: fakePassword()}, + }, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -232,3 +400,428 @@ func TestServer_SetPassword(t *testing.T) { }) } } + +func fakePassword() string { + return gofakeit.Password(true, true, true, true, false, 5) + "Password1!" +} + +func TestServer_RequestPasswordReset(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 + code bool + } + tests := []struct { + name string + ctx context.Context + dep func(req *user.RequestPasswordResetRequest) error + req *user.RequestPasswordResetRequest + res res + wantErr bool + }{ + { + name: "password reset, no context", + ctx: context.Background(), + dep: func(req *user.RequestPasswordResetRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RequestPasswordResetRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Medium: &user.RequestPasswordResetRequest_SendEmail{}, + }, + wantErr: true, + }, + { + name: "password reset, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.RequestPasswordResetRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RequestPasswordResetRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Medium: &user.RequestPasswordResetRequest_SendEmail{}, + }, + wantErr: true, + }, + { + name: "password reset, no password set", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RequestPasswordResetRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.RequestPasswordResetRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Medium: &user.RequestPasswordResetRequest_SendEmail{}, + }, + wantErr: true, + }, + { + name: "password reset, user not existing in org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RequestPasswordResetRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RequestPasswordResetRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + Medium: &user.RequestPasswordResetRequest_SendEmail{}, + }, + wantErr: true, + }, + { + name: "password reset, user not existing", + ctx: isolatedIAMOwnerCTX, + req: &user.RequestPasswordResetRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + Id: "not existing", + Medium: &user.RequestPasswordResetRequest_SendEmail{}, + }, + wantErr: true, + }, + { + name: "password reset, email, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RequestPasswordResetRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RequestPasswordResetRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Medium: &user.RequestPasswordResetRequest_SendEmail{}, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "password reset, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RequestPasswordResetRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RequestPasswordResetRequest{ + Medium: &user.RequestPasswordResetRequest_SendEmail{}, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "password reset, phone, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RequestPasswordResetRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RequestPasswordResetRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Medium: &user.RequestPasswordResetRequest_SendSms{}, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "password reset, code, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RequestPasswordResetRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RequestPasswordResetRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Medium: &user.RequestPasswordResetRequest_ReturnCode{}, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + code: true, + }, + }, + } + 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.RequestPasswordReset(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + integration.AssertResourceDetails(t, tt.res.want, got.Details) + if tt.res.code { + require.NotEmpty(t, got.VerificationCode) + } + }) + } +} + +func TestServer_RemovePassword(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.RemovePasswordRequest) error + req *user.RemovePasswordRequest + res res + wantErr bool + }{ + { + name: "password remove, no context", + ctx: context.Background(), + dep: func(req *user.RemovePasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RemovePasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "password remove, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.RemovePasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RemovePasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "password remove, no password set", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RemovePasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.RemovePasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "password remove, user not existing in org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RemovePasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RemovePasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + }, + wantErr: true, + }, + { + name: "password remove, user not existing", + ctx: isolatedIAMOwnerCTX, + req: &user.RemovePasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + Id: "not existing", + }, + wantErr: true, + }, + { + name: "password remove, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RemovePasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RemovePasswordRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "password remove, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RemovePasswordRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + instance.SetAuthenticatorPassword(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, fakePassword()) + return nil + }, + req: &user.RemovePasswordRequest{}, + 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.RemovePassword(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + integration.AssertResourceDetails(t, tt.res.want, got.Details) + }) + } +} diff --git a/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go b/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go index c8db0f7f6a..2564d73f06 100644 --- a/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/user_test.go @@ -8,6 +8,7 @@ import ( "github.com/brianvoe/gofakeit/v6" "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/logging" "google.golang.org/protobuf/types/known/structpb" @@ -212,16 +213,16 @@ func TestServer_CreateUser(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := instance.Client.UserV3Alpha.CreateUser(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) integration.AssertResourceDetails(t, tt.res.want, got.Details) if tt.res.returnCodeEmail { - require.NotNil(t, got.EmailCode) + assert.NotNil(t, got.EmailCode) } if tt.res.returnCodePhone { - require.NotNil(t, got.PhoneCode) + assert.NotNil(t, got.PhoneCode) } }) } @@ -628,16 +629,16 @@ func TestServer_PatchUser(t *testing.T) { } got, err := instance.Client.UserV3Alpha.PatchUser(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) integration.AssertResourceDetails(t, tt.res.want, got.Details) if tt.res.returnCodeEmail { - require.NotNil(t, got.EmailCode) + assert.NotNil(t, got.EmailCode) } if tt.res.returnCodePhone { - require.NotNil(t, got.PhoneCode) + assert.NotNil(t, got.PhoneCode) } }) } @@ -843,10 +844,10 @@ func TestServer_DeleteUser(t *testing.T) { } got, err := instance.Client.UserV3Alpha.DeleteUser(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) integration.AssertResourceDetails(t, tt.want, got.Details) }) } @@ -1054,10 +1055,10 @@ func TestServer_LockUser(t *testing.T) { } got, err := instance.Client.UserV3Alpha.LockUser(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) integration.AssertResourceDetails(t, tt.want, got.Details) }) } @@ -1237,10 +1238,10 @@ func TestServer_UnlockUser(t *testing.T) { } got, err := instance.Client.UserV3Alpha.UnlockUser(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) integration.AssertResourceDetails(t, tt.want, got.Details) }) } @@ -1439,10 +1440,10 @@ func TestServer_DeactivateUser(t *testing.T) { } got, err := instance.Client.UserV3Alpha.DeactivateUser(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) integration.AssertResourceDetails(t, tt.want, got.Details) }) } @@ -1622,10 +1623,10 @@ func TestServer_ActivateUser(t *testing.T) { } got, err := instance.Client.UserV3Alpha.ActivateUser(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) integration.AssertResourceDetails(t, tt.want, got.Details) }) } diff --git a/internal/api/grpc/resources/user/v3alpha/integration_test/username_test.go b/internal/api/grpc/resources/user/v3alpha/integration_test/username_test.go index e44eb30e4f..ed5f36f3ab 100644 --- a/internal/api/grpc/resources/user/v3alpha/integration_test/username_test.go +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/username_test.go @@ -8,7 +8,6 @@ import ( "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" @@ -369,10 +368,10 @@ func TestServer_AddUsername(t *testing.T) { } got, err := instance.Client.UserV3Alpha.AddUsername(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return } - require.NoError(t, err) + assert.NoError(t, err) integration.AssertResourceDetails(t, tt.res.want, got.Details) }) } @@ -412,8 +411,9 @@ func TestServer_DeleteUsername(t *testing.T) { ctx: context.Background(), dep: func(req *user.RemoveUsernameRequest) error { userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() usernameResp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), userResp.GetDetails().GetId(), gofakeit.Username(), false) - req.Id = usernameResp.GetUsernameId() + req.UsernameId = usernameResp.GetUsernameId() return nil }, req: &user.RemoveUsernameRequest{ @@ -430,8 +430,9 @@ func TestServer_DeleteUsername(t *testing.T) { ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), dep: func(req *user.RemoveUsernameRequest) error { userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() usernameResp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), userResp.GetDetails().GetId(), gofakeit.Username(), false) - req.Id = usernameResp.GetUsernameId() + req.UsernameId = usernameResp.GetUsernameId() return nil }, req: &user.RemoveUsernameRequest{ @@ -452,6 +453,20 @@ func TestServer_DeleteUsername(t *testing.T) { OrgId: orgResp.GetOrganizationId(), }, }, + UsernameId: "notempty", + }, + wantErr: true, + }, + { + name: "username delete, userid empty", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + req: &user.RemoveUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "notempty", }, wantErr: true, }, @@ -464,7 +479,25 @@ func TestServer_DeleteUsername(t *testing.T) { OrgId: orgResp.GetOrganizationId(), }, }, - Id: "notexisting", + UsernameId: "notempty", + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "username remove, no username", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RemoveUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.RemoveUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, }, wantErr: true, }, @@ -473,8 +506,9 @@ func TestServer_DeleteUsername(t *testing.T) { ctx: isolatedIAMOwnerCTX, dep: func(req *user.RemoveUsernameRequest) error { userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() usernameResp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), userResp.GetDetails().GetId(), gofakeit.Username(), false) - req.Id = usernameResp.GetUsernameId() + req.UsernameId = usernameResp.GetUsernameId() return nil }, req: &user.RemoveUsernameRequest{ @@ -501,8 +535,8 @@ func TestServer_DeleteUsername(t *testing.T) { userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) req.Id = userResp.GetDetails().GetId() resp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Username(), false) - req.Id = resp.GetUsernameId() - instance.RemoveAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), resp.GetUsernameId()) + req.UsernameId = resp.GetUsernameId() + instance.RemoveAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, req.UsernameId) return nil }, req: &user.RemoveUsernameRequest{ @@ -519,8 +553,9 @@ func TestServer_DeleteUsername(t *testing.T) { ctx: isolatedIAMOwnerCTX, dep: func(req *user.RemoveUsernameRequest) error { userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() usernameResp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), userResp.GetDetails().GetId(), gofakeit.Username(), true) - req.Id = usernameResp.GetUsernameId() + req.UsernameId = usernameResp.GetUsernameId() return nil }, req: &user.RemoveUsernameRequest{ @@ -549,11 +584,13 @@ func TestServer_DeleteUsername(t *testing.T) { } got, err := instance.Client.UserV3Alpha.RemoveUsername(tt.ctx, tt.req) if tt.wantErr { - require.Error(t, err) + assert.Error(t, err) return + } - require.NoError(t, err) + assert.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 index 128b9c7608..3b36a0114e 100644 --- a/internal/api/grpc/resources/user/v3alpha/password.go +++ b/internal/api/grpc/resources/user/v3alpha/password.go @@ -3,8 +3,11 @@ package user import ( "context" + "github.com/muhlemmer/gu" + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" ) @@ -29,23 +32,11 @@ func setPasswordRequestToSetSchemaUserPassword(req *user.SetPasswordRequest) *co Password: req.GetNewPassword().GetPassword(), EncodedPasswordHash: req.GetNewPassword().GetHash(), ChangeRequired: req.GetNewPassword().GetChangeRequired(), + VerificationCode: req.GetNewPassword().GetVerificationCode(), + CurrentPassword: req.GetNewPassword().GetCurrentPassword(), } } -/* -func (s *Server) RemovePassword(ctx context.Context, req *user.RemovePasswordRequest) (_ *user.RemovePasswordResponse, err error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - details, err := s.command.DeleteSchemaUserPassword(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId()) - if err != nil { - return nil, err - } - return &user.RemovePasswordResponse{ - Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), - }, nil -} - func (s *Server) RemovePassword(ctx context.Context, req *user.RemovePasswordRequest) (_ *user.RemovePasswordResponse, err error) { if err := checkUserSchemaEnabled(ctx); err != nil { return nil, err @@ -58,4 +49,35 @@ func (s *Server) RemovePassword(ctx context.Context, req *user.RemovePasswordReq Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), }, nil } -*/ + +func (s *Server) RequestPasswordReset(ctx context.Context, req *user.RequestPasswordResetRequest) (_ *user.RequestPasswordResetResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + schemauser := requestPasswordResetRequestToRequestSchemaUserPasswordReset(req) + details, err := s.command.RequestSchemaUserPasswordReset(ctx, schemauser) + if err != nil { + return nil, err + } + return &user.RequestPasswordResetResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + VerificationCode: gu.Ptr(schemauser.PlainCode), + }, nil +} + +func requestPasswordResetRequestToRequestSchemaUserPasswordReset(req *user.RequestPasswordResetRequest) *command.RequestSchemaUserPasswordReset { + var notificationType domain.NotificationType + if req.GetSendEmail() != nil { + notificationType = domain.NotificationTypeEmail + } + if req.GetSendSms() != nil { + notificationType = domain.NotificationTypeSms + } + return &command.RequestSchemaUserPasswordReset{ + ResourceOwner: organizationToUpdateResourceOwner(req.Organization), + UserID: req.GetId(), + URLTemplate: req.GetSendEmail().GetUrlTemplate(), + ReturnCode: req.GetReturnCode() != nil, + NotificationType: notificationType, + } +} diff --git a/internal/api/grpc/resources/user/v3alpha/username.go b/internal/api/grpc/resources/user/v3alpha/username.go index 6991e8a63c..0e4bc7257a 100644 --- a/internal/api/grpc/resources/user/v3alpha/username.go +++ b/internal/api/grpc/resources/user/v3alpha/username.go @@ -32,11 +32,11 @@ func addUsernameRequestToAddUsername(req *user.AddUsernameRequest) *command.AddU } } -func (s *Server) DeleteUsername(ctx context.Context, req *user.RemoveUsernameRequest) (_ *user.RemoveUsernameResponse, err error) { +func (s *Server) RemoveUsername(ctx context.Context, req *user.RemoveUsernameRequest) (_ *user.RemoveUsernameResponse, err error) { if err := checkUserSchemaEnabled(ctx); err != nil { return nil, err } - details, err := s.command.DeleteUsername(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId()) + details, err := s.command.DeleteUsername(ctx, organizationToUpdateResourceOwner(req.Organization), req.GetId(), req.GetUsernameId()) if err != nil { return nil, err } diff --git a/internal/command/debug_events_test.go b/internal/command/debug_events_test.go index e740de8574..21f0f66481 100644 --- a/internal/command/debug_events_test.go +++ b/internal/command/debug_events_test.go @@ -210,6 +210,7 @@ func TestCommands_CreateDebugEvents(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "dgb1", }, }, { @@ -243,6 +244,7 @@ func TestCommands_CreateDebugEvents(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "dgb1", }, }, { @@ -275,6 +277,7 @@ func TestCommands_CreateDebugEvents(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "dgb1", }, }, { @@ -324,6 +327,7 @@ func TestCommands_CreateDebugEvents(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "dgb1", }, }, } diff --git a/internal/command/instance_features_test.go b/internal/command/instance_features_test.go index e6b6bb4346..21fd638e4d 100644 --- a/internal/command/instance_features_test.go +++ b/internal/command/instance_features_test.go @@ -64,6 +64,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "instance1", }, }, { @@ -93,6 +94,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "instance1", }, }, { @@ -111,6 +113,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "instance1", }, }, { @@ -129,6 +132,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "instance1", }, }, { @@ -147,6 +151,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "instance1", }, }, { @@ -165,6 +170,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "instance1", }, }, { @@ -224,6 +230,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "instance1", }, }, { @@ -275,6 +282,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", + ID: "instance1", }, }, } diff --git a/internal/command/system_features_test.go b/internal/command/system_features_test.go index 9c5f4cc2a9..941d3c0a5e 100644 --- a/internal/command/system_features_test.go +++ b/internal/command/system_features_test.go @@ -61,6 +61,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", + ID: "SYSTEM", }, }, { @@ -79,6 +80,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", + ID: "SYSTEM", }, }, { @@ -97,6 +99,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", + ID: "SYSTEM", }, }, { @@ -115,6 +118,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", + ID: "SYSTEM", }, }, { @@ -133,6 +137,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", + ID: "SYSTEM", }, }, { @@ -192,6 +197,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", + ID: "SYSTEM", }, }, { @@ -253,6 +259,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) { }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", + ID: "SYSTEM", }, }, } diff --git a/internal/command/user_v3_password.go b/internal/command/user_v3_password.go index 5345145e46..e8eddc75b3 100644 --- a/internal/command/user_v3_password.go +++ b/internal/command/user_v3_password.go @@ -59,6 +59,7 @@ func (c *Commands) SetSchemaUserPassword(ctx context.Context, user *SetSchemaUse return nil, err } resourceOwner := existing.ResourceOwner + // when no password was set yet if existing.EncodedHash == "" { existingUser, err := c.getSchemaUserExists(ctx, user.ResourceOwner, user.UserID) if err != nil { @@ -79,7 +80,7 @@ func (c *Commands) SetSchemaUserPassword(ctx context.Context, user *SetSchemaUse } encodedPassword := schemaUser.EncodedPasswordHash - if user.Password != "" { + if encodedPassword == "" && user.Password != "" { encodedPassword, err = c.userPasswordHasher.Hash(user.Password) if err = convertPasswapErr(err); err != nil { return nil, err diff --git a/internal/command/user_v3_password_model.go b/internal/command/user_v3_password_model.go index 921d61e1b9..d1c66e873b 100644 --- a/internal/command/user_v3_password_model.go +++ b/internal/command/user_v3_password_model.go @@ -5,9 +5,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/repository/user/authenticator" - "github.com/zitadel/zitadel/internal/repository/user/schemauser" ) type PasswordV3WriteModel struct { @@ -45,7 +43,7 @@ func (wm *PasswordV3WriteModel) Reduce() error { wm.EncodedHash = "" wm.ChangeRequired = false wm.Code = nil - case *user.HumanPasswordCodeAddedEvent: + case *authenticator.PasswordCodeAddedEvent: wm.Code = e.Code wm.CodeCreationDate = e.CreationDate() wm.CodeExpiry = e.Expiry @@ -58,7 +56,7 @@ func (wm *PasswordV3WriteModel) Query() *eventstore.SearchQueryBuilder { return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(wm.ResourceOwner). AddQuery(). - AggregateTypes(schemauser.AggregateType). + AggregateTypes(authenticator.AggregateType). AggregateIDs(wm.AggregateID). EventTypes( authenticator.PasswordCreatedType, diff --git a/internal/command/user_v3_password_test.go b/internal/command/user_v3_password_test.go index 07a40aceba..ed27009964 100644 --- a/internal/command/user_v3_password_test.go +++ b/internal/command/user_v3_password_test.go @@ -4,8 +4,11 @@ import ( "context" "errors" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/zitadel/passwap" + "go.uber.org/mock/gomock" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" @@ -50,6 +53,7 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { eventstore func(t *testing.T) *eventstore.Eventstore userPasswordHasher *crypto.Hasher checkPermission domain.PermissionCheck + codeAlg crypto.EncryptionAlgorithm } type args struct { ctx context.Context @@ -117,7 +121,7 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { }, res{ err: func(err error) bool { - return errors.Is(err, zerrors.ThrowNotFound(nil, "TODO", "TODO")) + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-TODO", "Errors.User.Password.NotFound")) }, }, }, @@ -176,6 +180,39 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { }, }, }, + { + "password set, complexity failed", + 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, ok", fields{ @@ -242,6 +279,40 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { }, }, }, + { + "password set, encoded, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + filterPasswordComplexityPolicyExisting(), + expectPush( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "user1", + "$plain$x$password2", + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "passwordnotused", + EncodedPasswordHash: "$plain$x$password2", + ChangeRequired: false, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { "password set, current password, ok", fields{ @@ -258,7 +329,6 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckNotAllowed(), userPasswordHasher: mockPasswordHasher("x"), }, args{ @@ -277,7 +347,7 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { }, }, { - "password set, code, ok", + "password set, current password, ok", fields{ eventstore: expectEventstore( filterSchemaUserPasswordExisting(), @@ -292,15 +362,15 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckNotAllowed(), userPasswordHasher: mockPasswordHasher("x"), }, args{ ctx: authz.NewMockContext("instanceID", "", ""), user: &SetSchemaUserPassword{ - UserID: "user1", - Password: "password2", - ChangeRequired: false, + UserID: "user1", + Password: "password2", + CurrentPassword: "password", + ChangeRequired: false, }, }, res{ @@ -308,6 +378,160 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { ResourceOwner: "org1", }, }, + }, { + "password set, current password, failed", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + ), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "password2", + CurrentPassword: "notreally", + ChangeRequired: false, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(passwap.ErrPasswordMismatch, "COMMAND-3M0fs", "Errors.User.Password.Invalid")) + }, + }, + }, + { + "password set, code, ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "user1", + "$plain$x$password", + false, + ), + ), + eventFromEventPusherWithCreationDateNow( + authenticator.NewPasswordCodeAddedEvent(context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour*1, + domain.NotificationTypeEmail, + "", + false, + ), + ), + ), + filterPasswordComplexityPolicyExisting(), + expectPush( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "user1", + "$plain$x$password2", + false, + ), + ), + ), + userPasswordHasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "password2", + VerificationCode: "code", + ChangeRequired: false, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "password set, code, failed", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authenticator.NewPasswordCreatedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + "user1", + "$plain$x$password", + false, + ), + ), + eventFromEventPusherWithCreationDateNow( + authenticator.NewPasswordCodeAddedEvent(context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour*1, + domain.NotificationTypeEmail, + "", + false, + ), + ), + ), + ), + userPasswordHasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "password2", + VerificationCode: "notreally", + ChangeRequired: false, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid")) + }, + }, + }, + { + "password set, code, no code", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + ), + userPasswordHasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &SetSchemaUserPassword{ + UserID: "user1", + Password: "password2", + VerificationCode: "notreally", + ChangeRequired: false, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-TODO", "Errors.User.Code.NotFound")) + }, + }, }, } for _, tt := range tests { @@ -316,6 +540,7 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, userPasswordHasher: tt.fields.userPasswordHasher, + userEncryption: tt.fields.codeAlg, } details, err := c.SetSchemaUserPassword(tt.args.ctx, tt.args.user) if tt.res.err == nil { @@ -331,6 +556,224 @@ func TestCommands_SetSchemaUserPassword(t *testing.T) { } } +func TestCommands_RequestSchemaUserPasswordReset(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + newCode encrypedCodeFunc + } + type args struct { + ctx context.Context + user *RequestSchemaUserPasswordReset + } + type res struct { + details *domain.ObjectDetails + plainCode string + 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: &RequestSchemaUserPasswordReset{}, + }, + 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", "", ""), + user: &RequestSchemaUserPasswordReset{ + UserID: "notexisting", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-TODO", "Errors.User.Password.NotFound")) + }, + }, + }, + { + "no permission, error", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &RequestSchemaUserPasswordReset{ + UserID: "user1", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + "password reset, email, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + expectPush( + authenticator.NewPasswordCodeAddedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + 10*time.Minute, + domain.NotificationTypeEmail, + "https://example.com/password/changey?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("code", 10*time.Minute), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &RequestSchemaUserPasswordReset{ + UserID: "user1", + NotificationType: domain.NotificationTypeEmail, + URLTemplate: "https://example.com/password/changey?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "password reset, sms, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + expectPush( + authenticator.NewPasswordCodeAddedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + 10*time.Minute, + domain.NotificationTypeSms, + "", + false, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("code", 10*time.Minute), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &RequestSchemaUserPasswordReset{ + UserID: "user1", + NotificationType: domain.NotificationTypeSms, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "password reset, returned, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserPasswordExisting(), + expectPush( + authenticator.NewPasswordCodeAddedEvent( + context.Background(), + &authenticator.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + 10*time.Minute, + domain.NotificationTypeEmail, + "", + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("code", 10*time.Minute), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &RequestSchemaUserPasswordReset{ + UserID: "user1", + ReturnCode: true, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + plainCode: "code", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + newEncryptedCode: tt.fields.newCode, + } + details, err := c.RequestSchemaUserPasswordReset(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) + } + if tt.res.plainCode != "" { + assert.Equal(t, tt.res.plainCode, tt.args.user.PlainCode) + } + }) + } +} + func TestCommands_DeleteSchemaUserPassword(t *testing.T) { type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore diff --git a/internal/command/user_v3_username.go b/internal/command/user_v3_username.go index ea2f1e0f87..8c8ba30d39 100644 --- a/internal/command/user_v3_username.go +++ b/internal/command/user_v3_username.go @@ -39,8 +39,8 @@ func (c *Commands) AddUsername(ctx context.Context, username *AddUsername) (*dom return pushedEventsToObjectDetails(events), nil } -func (c *Commands) DeleteUsername(ctx context.Context, resourceOwner, id string) (_ *domain.ObjectDetails, err error) { - existing, err := c.getSchemaUsernameExistsWithPermission(ctx, resourceOwner, id) +func (c *Commands) DeleteUsername(ctx context.Context, resourceOwner, userID, id string) (_ *domain.ObjectDetails, err error) { + existing, err := c.getSchemaUsernameExistsWithPermission(ctx, resourceOwner, userID, id) if err != nil { return nil, err } @@ -57,11 +57,14 @@ func (c *Commands) DeleteUsername(ctx context.Context, resourceOwner, id string) return pushedEventsToObjectDetails(events), nil } -func (c *Commands) getSchemaUsernameExistsWithPermission(ctx context.Context, resourceOwner, id string) (*UsernameV3WriteModel, error) { +func (c *Commands) getSchemaUsernameExistsWithPermission(ctx context.Context, resourceOwner, userID, id string) (*UsernameV3WriteModel, error) { + if userID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-J6ybG5WZiy", "Errors.IDMissing") + } if id == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-PoSU5BOZCi", "Errors.IDMissing") } - writeModel := NewUsernameV3WriteModel(resourceOwner, id) + writeModel := NewUsernameV3WriteModel(resourceOwner, userID, id) if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { return nil, err } diff --git a/internal/command/user_v3_username_model.go b/internal/command/user_v3_username_model.go index b8cffcca66..8f002a1b51 100644 --- a/internal/command/user_v3_username_model.go +++ b/internal/command/user_v3_username_model.go @@ -3,7 +3,6 @@ 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 UsernameV3WriteModel struct { @@ -13,12 +12,13 @@ type UsernameV3WriteModel struct { IsOrgSpecific bool } -func NewUsernameV3WriteModel(resourceOwner, userID string) *UsernameV3WriteModel { +func NewUsernameV3WriteModel(resourceOwner, userID, id string) *UsernameV3WriteModel { return &UsernameV3WriteModel{ WriteModel: eventstore.WriteModel{ - AggregateID: userID, + AggregateID: id, ResourceOwner: resourceOwner, }, + UserID: userID, } } @@ -26,6 +26,9 @@ func (wm *UsernameV3WriteModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *authenticator.UsernameCreatedEvent: + if e.UserID != wm.UserID { + continue + } wm.UserID = e.UserID wm.Username = e.Username wm.IsOrgSpecific = e.IsOrgSpecific @@ -42,7 +45,7 @@ func (wm *UsernameV3WriteModel) Query() *eventstore.SearchQueryBuilder { return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(wm.ResourceOwner). AddQuery(). - AggregateTypes(schemauser.AggregateType). + AggregateTypes(authenticator.AggregateType). AggregateIDs(wm.AggregateID). EventTypes( authenticator.UsernameCreatedType, diff --git a/internal/command/user_v3_username_test.go b/internal/command/user_v3_username_test.go index f5ad96e124..fb2df9722c 100644 --- a/internal/command/user_v3_username_test.go +++ b/internal/command/user_v3_username_test.go @@ -63,7 +63,7 @@ func filterUsernameExisting(isOrgSpecifc bool) expect { authenticator.NewUsernameCreatedEvent( context.Background(), &authenticator.NewAggregate("username1", "org1").Aggregate, - "id1", + "user1", isOrgSpecifc, "username", ), @@ -264,6 +264,7 @@ func TestCommands_DeleteUsername(t *testing.T) { type args struct { ctx context.Context resourceOwner string + userID string id string } type res struct { @@ -277,7 +278,7 @@ func TestCommands_DeleteUsername(t *testing.T) { res res }{ { - "no ID, error", + "no userID, error", fields{ eventstore: expectEventstore(), checkPermission: newMockPermissionCheckAllowed(), @@ -286,6 +287,23 @@ func TestCommands_DeleteUsername(t *testing.T) { ctx: authz.NewMockContext("instanceID", "", ""), id: "", }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-J6ybG5WZiy", "Errors.IDMissing")) + }, + }, + }, + { + "no ID, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + userID: "user1", + id: "", + }, res{ err: func(err error) bool { return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-PoSU5BOZCi", "Errors.IDMissing")) @@ -301,8 +319,9 @@ func TestCommands_DeleteUsername(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args{ - ctx: authz.NewMockContext("instanceID", "", ""), - id: "notexisting", + ctx: authz.NewMockContext("instanceID", "", ""), + userID: "user1", + id: "notexisting", }, res{ err: func(err error) bool { @@ -319,7 +338,7 @@ func TestCommands_DeleteUsername(t *testing.T) { authenticator.NewUsernameCreatedEvent( context.Background(), &authenticator.NewAggregate("username1", "org1").Aggregate, - "id1", + "user1", true, "username", ), @@ -337,8 +356,9 @@ func TestCommands_DeleteUsername(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args{ - ctx: authz.NewMockContext("instanceID", "", ""), - id: "notexisting", + ctx: authz.NewMockContext("instanceID", "", ""), + userID: "user1", + id: "notexisting", }, res{ err: func(err error) bool { @@ -355,8 +375,9 @@ func TestCommands_DeleteUsername(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args{ - ctx: authz.NewMockContext("instanceID", "", ""), - id: "username1", + ctx: authz.NewMockContext("instanceID", "", ""), + userID: "user1", + id: "username1", }, res{ err: func(err error) bool { @@ -381,8 +402,9 @@ func TestCommands_DeleteUsername(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args{ - ctx: authz.NewMockContext("instanceID", "", ""), - id: "username1", + ctx: authz.NewMockContext("instanceID", "", ""), + userID: "user1", + id: "username1", }, res{ details: &domain.ObjectDetails{ @@ -407,8 +429,9 @@ func TestCommands_DeleteUsername(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args{ - ctx: authz.NewMockContext("instanceID", "", ""), - id: "username1", + ctx: authz.NewMockContext("instanceID", "", ""), + userID: "user1", + id: "username1", }, res{ details: &domain.ObjectDetails{ @@ -423,7 +446,7 @@ func TestCommands_DeleteUsername(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - details, err := c.DeleteUsername(tt.args.ctx, tt.args.resourceOwner, tt.args.id) + details, err := c.DeleteUsername(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.id) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/integration/client.go b/internal/integration/client.go index f50bce8a68..db319979a6 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -789,8 +789,40 @@ func (i *Instance) AddAuthenticatorUsername(ctx context.Context, orgID string, u return user } -func (i *Instance) RemoveAuthenticatorUsername(ctx context.Context, orgID string, id string) *user_v3alpha.RemoveUsernameResponse { +func (i *Instance) RemoveAuthenticatorUsername(ctx context.Context, orgID string, userid, id string) *user_v3alpha.RemoveUsernameResponse { user, err := i.Client.UserV3Alpha.RemoveUsername(ctx, &user_v3alpha.RemoveUsernameRequest{ + Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}}, + Id: userid, + UsernameId: id, + }) + logging.OnError(err).Fatal("remove username") + return user +} + +func (i *Instance) SetAuthenticatorPassword(ctx context.Context, orgID string, userID string, password string) *user_v3alpha.SetPasswordResponse { + user, err := i.Client.UserV3Alpha.SetPassword(ctx, &user_v3alpha.SetPasswordRequest{ + Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}}, + Id: userID, + NewPassword: &user_v3alpha.SetPassword{ + Type: &user_v3alpha.SetPassword_Password{Password: password}, + }, + }) + logging.OnError(err).Fatal("create password") + return user +} + +func (i *Instance) RequestAuthenticatorPasswordReset(ctx context.Context, orgID string, userID string) *user_v3alpha.RequestPasswordResetResponse { + user, err := i.Client.UserV3Alpha.RequestPasswordReset(ctx, &user_v3alpha.RequestPasswordResetRequest{ + Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}}, + Id: userID, + Medium: &user_v3alpha.RequestPasswordResetRequest_ReturnCode{}, + }) + logging.OnError(err).Fatal("reset password") + return user +} + +func (i *Instance) RemoveAuthenticatorPassword(ctx context.Context, orgID string, id string) *user_v3alpha.RemovePasswordResponse { + user, err := i.Client.UserV3Alpha.RemovePassword(ctx, &user_v3alpha.RemovePasswordRequest{ Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}}, Id: id, }) diff --git a/internal/repository/user/authenticator/eventstore.go b/internal/repository/user/authenticator/eventstore.go index c4f12c7057..48b10f7b25 100644 --- a/internal/repository/user/authenticator/eventstore.go +++ b/internal/repository/user/authenticator/eventstore.go @@ -6,6 +6,7 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, UsernameCreatedType, eventstore.GenericEventMapper[UsernameCreatedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, UsernameDeletedType, eventstore.GenericEventMapper[UsernameDeletedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, PasswordCreatedType, eventstore.GenericEventMapper[PasswordCreatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, PasswordCodeAddedType, eventstore.GenericEventMapper[PasswordCodeAddedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, PasswordDeletedType, eventstore.GenericEventMapper[PasswordDeletedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, JWTCreatedType, eventstore.GenericEventMapper[JWTCreatedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, JWTDeletedType, eventstore.GenericEventMapper[JWTDeletedEvent]) diff --git a/proto/zitadel/resources/user/v3alpha/user_service.proto b/proto/zitadel/resources/user/v3alpha/user_service.proto index cd78527e05..1d6f38aced 100644 --- a/proto/zitadel/resources/user/v3alpha/user_service.proto +++ b/proto/zitadel/resources/user/v3alpha/user_service.proto @@ -1540,6 +1540,16 @@ message RemoveUsernameRequest { example: "\"69629026806489455\""; } ]; + // unique identifier of the username. + string username_id = 4 [ + (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: "\"69629023906488334\""; + } + ]; } message RemoveUsernameResponse {