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 new file mode 100644 index 0000000000..e44eb30e4f --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/integration_test/username_test.go @@ -0,0 +1,559 @@ +//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_AddUsername(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.AddUsernameRequest) error + req *user.AddUsernameRequest + res res + wantErr bool + }{ + { + name: "username add, no context", + ctx: context.Background(), + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + Username: gofakeit.Username(), + IsOrganizationSpecific: false, + }, + }, + wantErr: true, + }, + { + name: "username add, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + Username: gofakeit.Username(), + IsOrganizationSpecific: false, + }, + }, + wantErr: true, + }, + { + name: "username add, username empty", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + Username: "", + IsOrganizationSpecific: false, + }, + }, + wantErr: true, + }, + { + name: "username add, user not existing in org", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: "notexisting", + }, + }, + Username: &user.SetUsername{ + Username: gofakeit.Username(), + IsOrganizationSpecific: false, + }, + }, + wantErr: true, + }, + { + name: "username add, user not existing", + ctx: isolatedIAMOwnerCTX, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "notexisting", + Username: &user.SetUsername{ + Username: gofakeit.Username(), + IsOrganizationSpecific: false, + }, + }, + wantErr: true, + }, + { + name: "username add, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + Username: gofakeit.Username(), + IsOrganizationSpecific: false, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "username add, no org, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.AddUsernameRequest{ + Username: &user.SetUsername{ + Username: gofakeit.Username(), + IsOrganizationSpecific: false, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "username add, already existing", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + username := gofakeit.Username() + instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, username, false) + req.Username.Username = username + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + IsOrganizationSpecific: false, + }, + }, + wantErr: true, + }, + { + name: "username add, isOrgSpecific, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + Username: gofakeit.Username(), + IsOrganizationSpecific: true, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "username add, already existing", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + username := gofakeit.Username() + instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, username, false) + req.Username.Username = username + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + IsOrganizationSpecific: false, + }, + }, + wantErr: true, + }, + { + name: "username add, isOrgSpecific, already existing", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + username := gofakeit.Username() + instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, username, true) + req.Username.Username = username + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + IsOrganizationSpecific: true, + }, + }, + wantErr: true, + }, + { + name: "username add, isOrgSpecific existing, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + username := gofakeit.Username() + instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, username, true) + req.Username.Username = username + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + IsOrganizationSpecific: false, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "username add, existing, isOrgSpecific ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.AddUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.Id = userResp.GetDetails().GetId() + username := gofakeit.Username() + instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, username, false) + req.Username.Username = username + return nil + }, + req: &user.AddUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Username: &user.SetUsername{ + IsOrganizationSpecific: true, + }, + }, + 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.AddUsername(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.res.want, got.Details) + }) + } +} + +func TestServer_DeleteUsername(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.RemoveUsernameRequest) error + req *user.RemoveUsernameRequest + res res + wantErr bool + }{ + { + name: "username delete, no context", + ctx: context.Background(), + dep: func(req *user.RemoveUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + usernameResp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), userResp.GetDetails().GetId(), gofakeit.Username(), false) + req.Id = usernameResp.GetUsernameId() + return nil + }, + req: &user.RemoveUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "username delete, no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + dep: func(req *user.RemoveUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + usernameResp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), userResp.GetDetails().GetId(), gofakeit.Username(), false) + req.Id = usernameResp.GetUsernameId() + return nil + }, + req: &user.RemoveUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "username remove, id empty", + ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin), + req: &user.RemoveUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "username remove, not existing", + ctx: isolatedIAMOwnerCTX, + req: &user.RemoveUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "username remove, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RemoveUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + usernameResp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), userResp.GetDetails().GetId(), gofakeit.Username(), false) + req.Id = usernameResp.GetUsernameId() + return nil + }, + req: &user.RemoveUsernameRequest{ + 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: "username remove, already removed", + 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() + resp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), req.Id, gofakeit.Username(), false) + req.Id = resp.GetUsernameId() + instance.RemoveAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), resp.GetUsernameId()) + return nil + }, + req: &user.RemoveUsernameRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "username remove, isOrgSpecific, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(req *user.RemoveUsernameRequest) error { + userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + usernameResp := instance.AddAuthenticatorUsername(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), userResp.GetDetails().GetId(), gofakeit.Username(), true) + req.Id = usernameResp.GetUsernameId() + return nil + }, + req: &user.RemoveUsernameRequest{ + 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(), + }, + }, + }, + }, + } + 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.RemoveUsername(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/username.go b/internal/api/grpc/resources/user/v3alpha/username.go new file mode 100644 index 0000000000..6991e8a63c --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/username.go @@ -0,0 +1,46 @@ +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) AddUsername(ctx context.Context, req *user.AddUsernameRequest) (_ *user.AddUsernameResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + details, err := s.command.AddUsername(ctx, addUsernameRequestToAddUsername(req)) + if err != nil { + return nil, err + } + return &user.AddUsernameResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + UsernameId: details.ID, + }, nil +} + +func addUsernameRequestToAddUsername(req *user.AddUsernameRequest) *command.AddUsername { + return &command.AddUsername{ + ResourceOwner: organizationToUpdateResourceOwner(req.Organization), + UserID: req.GetId(), + Username: req.GetUsername().GetUsername(), + IsOrgSpecific: req.GetUsername().GetIsOrganizationSpecific(), + } +} + +func (s *Server) DeleteUsername(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()) + if err != nil { + return nil, err + } + return &user.RemoveUsernameResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + }, nil +} diff --git a/internal/command/converter.go b/internal/command/converter.go index e4309a54c1..86f308bd23 100644 --- a/internal/command/converter.go +++ b/internal/command/converter.go @@ -19,5 +19,6 @@ func pushedEventsToObjectDetails(events []eventstore.Event) *domain.ObjectDetail Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreatedAt(), ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + ID: events[len(events)-1].Aggregate().ID, } } diff --git a/internal/command/user_v3_username.go b/internal/command/user_v3_username.go index 06d023b9d3..e072ef9247 100644 --- a/internal/command/user_v3_username.go +++ b/internal/command/user_v3_username.go @@ -9,8 +9,6 @@ import ( ) type AddUsername struct { - Details *domain.ObjectDetails - ResourceOwner string UserID string @@ -18,14 +16,14 @@ type AddUsername struct { IsOrgSpecific bool } -func (c *Commands) AddUsername(ctx context.Context, username *AddUsername) (err error) { +func (c *Commands) AddUsername(ctx context.Context, username *AddUsername) (*domain.ObjectDetails, error) { existing, err := existingSchemaUserWithPermission(ctx, c, username.ResourceOwner, username.UserID) if err != nil { - return err + return nil, err } id, err := c.idGenerator.Next() if err != nil { - return err + return nil, err } events, err := c.eventstore.Push(ctx, authenticator.NewUsernameCreatedEvent(ctx, @@ -36,20 +34,16 @@ func (c *Commands) AddUsername(ctx context.Context, username *AddUsername) (err ), ) if err != nil { - return err + return nil, err } - username.Details = pushedEventsToObjectDetails(events) - return nil + return pushedEventsToObjectDetails(events), nil } func (c *Commands) DeleteUsername(ctx context.Context, resourceOwner, id string) (_ *domain.ObjectDetails, err error) { - existing, err := c.getSchemaUsernameExists(ctx, resourceOwner, id) + existing, err := c.getSchemaUsernameExistsWithPermission(ctx, resourceOwner, id) if err != nil { return nil, err } - if existing.Username == "" { - return nil, zerrors.ThrowNotFound(nil, "TODO", "TODO") - } events, err := c.eventstore.Push(ctx, authenticator.NewUsernameDeletedEvent(ctx, &authenticator.NewAggregate(id, existing.ResourceOwner).Aggregate, @@ -63,11 +57,21 @@ func (c *Commands) DeleteUsername(ctx context.Context, resourceOwner, id string) return pushedEventsToObjectDetails(events), nil } -func (c *Commands) getSchemaUsernameExists(ctx context.Context, resourceOwner, id string) (*UsernameV3WriteModel, error) { +func (c *Commands) getSchemaUsernameExistsWithPermission(ctx context.Context, resourceOwner, id string) (*UsernameV3WriteModel, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-PoSU5BOZCi", "Errors.IDMissing") + } writeModel := NewUsernameV3WriteModel(resourceOwner, id) if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { return nil, err } + if writeModel.Username == "" { + return nil, zerrors.ThrowNotFound(nil, "TODO", "TODO") + } + + if err := c.checkPermissionUpdateUser(ctx, writeModel.ResourceOwner, writeModel.UserID); err != nil { + return nil, err + } return writeModel, nil } @@ -83,15 +87,18 @@ func existingSchemaUserWithPermission(ctx context.Context, c *Commands, resource return nil, zerrors.ThrowNotFound(nil, "COMMAND-6T2xrOHxTx", "Errors.User.NotFound") } - _, err = c.getSchemaWriteModelByID(ctx, "", existingUser.SchemaID) - 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 } diff --git a/internal/command/user_v3_username_model.go b/internal/command/user_v3_username_model.go index 765326faa4..b8cffcca66 100644 --- a/internal/command/user_v3_username_model.go +++ b/internal/command/user_v3_username_model.go @@ -8,6 +8,7 @@ import ( type UsernameV3WriteModel struct { eventstore.WriteModel + UserID string Username string IsOrgSpecific bool } @@ -25,9 +26,11 @@ func (wm *UsernameV3WriteModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *authenticator.UsernameCreatedEvent: + wm.UserID = e.UserID wm.Username = e.Username wm.IsOrgSpecific = e.IsOrgSpecific case *authenticator.UsernameDeletedEvent: + wm.UserID = "" wm.Username = "" wm.IsOrgSpecific = false } diff --git a/internal/command/user_v3_username_test.go b/internal/command/user_v3_username_test.go new file mode 100644 index 0000000000..f581577a93 --- /dev/null +++ b/internal/command/user_v3_username_test.go @@ -0,0 +1,438 @@ +package command + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/id" + "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/user/authenticator" + "github.com/zitadel/zitadel/internal/repository/user/schema" + "github.com/zitadel/zitadel/internal/repository/user/schemauser" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func filterSchemaUserExisting() expect { + return expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "id1", + 1, + json.RawMessage(`{ + "name": "user1" + }`), + ), + ), + ) +} + +func filterSchemaExisting() expect { + return expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ) +} + +func filterUsernameExisting(isOrgSpecifc bool) expect { + return expectFilter( + eventFromEventPusher( + authenticator.NewUsernameCreatedEvent( + context.Background(), + &authenticator.NewAggregate("username1", "org1").Aggregate, + "id1", + isOrgSpecifc, + "username", + ), + ), + ) +} + +func TestCommands_AddUsername(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + user *AddUsername + } + 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: &AddUsername{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing")) + }, + }, + }, + { + "user not existing, error", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &AddUsername{ + UserID: "notexisting", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-6T2xrOHxTx", "Errors.User.NotFound")) + }, + }, + }, + { + "no permission, error", + fields{ + eventstore: expectEventstore( + filterSchemaUserExisting(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &AddUsername{ + UserID: "user1", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + "userschema not existing, error", + fields{ + eventstore: expectEventstore( + filterSchemaUserExisting(), + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &AddUsername{ + UserID: "user1", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-6T2xrOHxTx", "TODO")) + }, + }, + }, + { + "username added, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserExisting(), + filterSchemaExisting(), + expectPush( + authenticator.NewUsernameCreatedEvent( + context.Background(), + &authenticator.NewAggregate("username1", "org1").Aggregate, + "user1", + false, + "username", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "username1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &AddUsername{ + UserID: "user1", + Username: "username", + IsOrgSpecific: false, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "username added, isOrgSpecific, ok", + fields{ + eventstore: expectEventstore( + filterSchemaUserExisting(), + filterSchemaExisting(), + expectPush( + authenticator.NewUsernameCreatedEvent( + context.Background(), + &authenticator.NewAggregate("username1", "org1").Aggregate, + "user1", + true, + "username", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "username1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &AddUsername{ + UserID: "user1", + Username: "username", + IsOrgSpecific: 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), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, + } + details, err := c.AddUsername(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_DeleteUsername(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")) + }, + }, + }, + { + "username 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")) + }, + }, + }, + { + "username already removed, error", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authenticator.NewUsernameCreatedEvent( + context.Background(), + &authenticator.NewAggregate("username1", "org1").Aggregate, + "id1", + true, + "username", + ), + ), + eventFromEventPusher( + authenticator.NewUsernameDeletedEvent( + context.Background(), + &authenticator.NewAggregate("username1", "org1").Aggregate, + true, + "username", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "notexisting", + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "TODO", "TODO")) + }, + }, + }, + { + "no permission, error", + fields{ + eventstore: expectEventstore( + filterUsernameExisting(false), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "username1", + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + "username removed, ok", + fields{ + eventstore: expectEventstore( + filterUsernameExisting(false), + expectPush( + authenticator.NewUsernameDeletedEvent( + context.Background(), + &authenticator.NewAggregate("username1", "org1").Aggregate, + false, + "username", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "username1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "username added, isOrgSpecific, ok", + fields{ + eventstore: expectEventstore( + filterUsernameExisting(true), + expectPush( + authenticator.NewUsernameDeletedEvent( + context.Background(), + &authenticator.NewAggregate("username1", "org1").Aggregate, + true, + "username", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + id: "username1", + }, + 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.DeleteUsername(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/integration/client.go b/internal/integration/client.go index 9bf855f5ce..f50bce8a68 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -776,6 +776,28 @@ func (i *Instance) CreateSchemaUser(ctx context.Context, orgID string, schemaID return user } +func (i *Instance) AddAuthenticatorUsername(ctx context.Context, orgID string, userID string, username string, isOrgSpecific bool) *user_v3alpha.AddUsernameResponse { + user, err := i.Client.UserV3Alpha.AddUsername(ctx, &user_v3alpha.AddUsernameRequest{ + Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}}, + Id: userID, + Username: &user_v3alpha.SetUsername{ + Username: username, + IsOrganizationSpecific: isOrgSpecific, + }, + }) + logging.OnError(err).Fatal("create username") + return user +} + +func (i *Instance) RemoveAuthenticatorUsername(ctx context.Context, orgID string, 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: id, + }) + logging.OnError(err).Fatal("remove username") + return user +} + func (i *Instance) CreateInviteCode(ctx context.Context, userID string) *user_v2.CreateInviteCodeResponse { user, err := i.Client.UserV2.CreateInviteCode(ctx, &user_v2.CreateInviteCodeRequest{ UserId: userID, diff --git a/proto/zitadel/resources/user/v3alpha/user_service.proto b/proto/zitadel/resources/user/v3alpha/user_service.proto index 91831bdc40..963b3e1fe9 100644 --- a/proto/zitadel/resources/user/v3alpha/user_service.proto +++ b/proto/zitadel/resources/user/v3alpha/user_service.proto @@ -1516,16 +1516,6 @@ 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 {