From 6889d6a1daba1ce6e9696f43a6d4c0fe1b3cb400 Mon Sep 17 00:00:00 2001 From: alfa-alex <76205862+alfa-alex@users.noreply.github.com> Date: Wed, 21 May 2025 12:55:40 +0200 Subject: [PATCH] feat: add custom org ID to AddOrganizationRequest (#9720) # Which Problems Are Solved - It is not possible to specify a custom organization ID when creating an organization. According to https://github.com/zitadel/zitadel/discussions/9202#discussioncomment-11929464 this is "an inconsistency in the V2 API". # How the Problems Are Solved - Adds the `org_id` as an optional parameter to the `AddOrganizationRequest` in the `v2beta` API. # Additional Changes None. # Additional Context - Discussion [#9202](https://github.com/zitadel/zitadel/discussions/9202) - I was mostly interested in how much work it'd be to add this field. Then after completing this, I thought I'd submit this PR. I won't be angry if you just close this PR with the reasoning "we didn't ask for it". :smile: - Even though I don't think this is a breaking change, I didn't add this to the `v2` API yet (don't know what the process for this is TBH). The changes should be analogous, so if you want me to, just request it. --------- Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> --- .../grpc/org/v2/integration_test/org_test.go | 12 ++++ .../org/v2/integration_test/query_test.go | 39 +++++++++++++ internal/api/grpc/org/v2/org.go | 1 + internal/api/grpc/org/v2/org_test.go | 15 +++++ .../org/v2beta/integration_test/org_test.go | 12 ++++ internal/api/grpc/org/v2beta/org.go | 1 + internal/api/grpc/org/v2beta/org_test.go | 15 +++++ internal/command/org.go | 21 ++++++- internal/command/org_test.go | 55 +++++++++++++++++++ internal/integration/client.go | 9 +++ proto/zitadel/org/v2/org_service.proto | 8 +++ proto/zitadel/org/v2beta/org_service.proto | 8 +++ 12 files changed, 193 insertions(+), 3 deletions(-) diff --git a/internal/api/grpc/org/v2/integration_test/org_test.go b/internal/api/grpc/org/v2/integration_test/org_test.go index aa8a718e68..b28bbf5ef2 100644 --- a/internal/api/grpc/org/v2/integration_test/org_test.go +++ b/internal/api/grpc/org/v2/integration_test/org_test.go @@ -81,6 +81,18 @@ func TestServer_AddOrganization(t *testing.T) { }, wantErr: true, }, + { + name: "no admin, custom org ID", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: gofakeit.AppName(), + OrgId: gu.Ptr("custom-org-ID"), + }, + want: &org.AddOrganizationResponse{ + OrganizationId: "custom-org-ID", + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, + }, + }, { name: "admin with init with userID passed for Human admin", ctx: CTX, diff --git a/internal/api/grpc/org/v2/integration_test/query_test.go b/internal/api/grpc/org/v2/integration_test/query_test.go index cb7576455c..b2f27dbe62 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -38,6 +38,16 @@ func createOrganization(ctx context.Context, name string) orgAttr { } } +func createOrganizationWithCustomOrgID(ctx context.Context, name string, orgID string) orgAttr { + orgResp := Instance.CreateOrganizationWithCustomOrgID(ctx, name, orgID) + orgResp.Details.CreationDate = orgResp.Details.ChangeDate + return orgAttr{ + ID: orgResp.GetOrganizationId(), + Name: name, + Details: orgResp.GetDetails(), + } +} + func TestServer_ListOrganizations(t *testing.T) { type args struct { ctx context.Context @@ -163,6 +173,35 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, + { + name: "list org by custom id, ok", + args: args{ + CTX, + &org.ListOrganizationsRequest{}, + func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { + orgs := make([]orgAttr, 1) + name := fmt.Sprintf("ListOrgs-%s", gofakeit.AppName()) + orgID := gofakeit.Company() + orgs[0] = createOrganizationWithCustomOrgID(ctx, name, orgID) + request.Queries = []*org.SearchQuery{ + OrganizationIdQuery(orgID), + } + return orgs, nil + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + }, + }, + }, + }, { name: "list org by name, ok", args: args{ diff --git a/internal/api/grpc/org/v2/org.go b/internal/api/grpc/org/v2/org.go index 5f21f7403e..bbc3caca85 100644 --- a/internal/api/grpc/org/v2/org.go +++ b/internal/api/grpc/org/v2/org.go @@ -31,6 +31,7 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, + OrgID: request.GetOrgId(), }, nil } diff --git a/internal/api/grpc/org/v2/org_test.go b/internal/api/grpc/org/v2/org_test.go index b384f858de..7ae252a209 100644 --- a/internal/api/grpc/org/v2/org_test.go +++ b/internal/api/grpc/org/v2/org_test.go @@ -38,6 +38,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { }, wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), }, + { + name: "custom org ID", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "custom org ID", + OrgId: gu.Ptr("org-ID"), + }, + }, + want: &command.OrgSetup{ + Name: "custom org ID", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{}, + OrgID: "org-ID", + }, + }, { name: "user ID", args: args{ diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index 5998b17a71..a2b2bf6047 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -79,6 +79,18 @@ func TestServer_AddOrganization(t *testing.T) { }, wantErr: true, }, + { + name: "no admin, custom org ID", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: gofakeit.AppName(), + OrgId: gu.Ptr("custom-org-ID"), + }, + want: &org.AddOrganizationResponse{ + OrganizationId: "custom-org-ID", + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, + }, + }, { name: "admin with init", ctx: CTX, diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index ab2da2b766..39730f827e 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -31,6 +31,7 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, + OrgID: request.GetOrgId(), }, nil } diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 5024b59c1d..57ed05dfb2 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -39,6 +39,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { }, wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), }, + { + name: "custom org ID", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "custom org ID", + OrgId: gu.Ptr("org-ID"), + }, + }, + want: &command.OrgSetup{ + Name: "custom org ID", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{}, + OrgID: "org-ID", + }, + }, { name: "user ID", args: args{ diff --git a/internal/command/org.go b/internal/command/org.go index a018a90c82..9874410a5f 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -31,6 +31,7 @@ type OrgSetup struct { Name string CustomDomain string Admins []*OrgSetupAdmin + OrgID string } // OrgSetupAdmin describes a user to be created (Human / Machine) or an existing (ID) to be used for an org setup. @@ -64,6 +65,13 @@ type CreatedOrgAdmin struct { MachineKey *MachineKey } +func (o *OrgSetup) Validate() (err error) { + if o.OrgID != "" && strings.TrimSpace(o.OrgID) == "" { + return zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument") + } + return nil +} + func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, allowInitialMail bool, userIDs ...string) (_ *CreatedOrg, err error) { cmds := c.newOrgSetupCommands(ctx, orgID, o) for _, admin := range o.Admins { @@ -233,12 +241,19 @@ func (c *orgSetupCommands) createdMachineAdmin(admin *OrgSetupAdmin) *CreatedOrg } func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail bool, userIDs ...string) (*CreatedOrg, error) { - orgID, err := c.idGenerator.Next() - if err != nil { + if err := o.Validate(); err != nil { return nil, err } - return c.setUpOrgWithIDs(ctx, o, orgID, allowInitialMail, userIDs...) + if o.OrgID == "" { + var err error + o.OrgID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + + return c.setUpOrgWithIDs(ctx, o, o.OrgID, allowInitialMail, userIDs...) } // AddOrgCommand defines the commands to create a new org, diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 4ec85d61e1..4b6fd7afe5 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -1344,6 +1344,22 @@ func TestCommandSide_SetUpOrg(t *testing.T) { err: zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"), }, }, + { + name: "org id empty, error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: " ", + }, + }, + res: res{ + err: zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument"), + }, + }, { name: "userID not existing, error", fields: fields{ @@ -1523,6 +1539,45 @@ func TestCommandSide_SetUpOrg(t *testing.T) { }, }, }, + { + name: "no human added, custom org ID", + fields: fields{ + eventstore: expectEventstore( + expectPush( + eventFromEventPusher(org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "Org", + )), + eventFromEventPusher(org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + ), + ), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: "custom-org-ID", + }, + }, + res: res{ + createdOrg: &CreatedOrg{ + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "custom-org-ID", + }, + CreatedAdmins: []*CreatedOrgAdmin{}, + }, + }, + }, { name: "existing human added", fields: fields{ diff --git a/internal/integration/client.go b/internal/integration/client.go index bd9775a28a..61645cc067 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -293,6 +293,15 @@ func SetOrgID(ctx context.Context, orgID string) context.Context { return metadata.NewOutgoingContext(ctx, md) } +func (i *Instance) CreateOrganizationWithCustomOrgID(ctx context.Context, name, orgID string) *org.AddOrganizationResponse { + resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ + Name: name, + OrgId: gu.Ptr(orgID), + }) + logging.OnError(err).Fatal("create org") + return resp +} + func (i *Instance) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse { resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ Name: name, diff --git a/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto index 94ced55146..729350e1f9 100644 --- a/proto/zitadel/org/v2/org_service.proto +++ b/proto/zitadel/org/v2/org_service.proto @@ -197,6 +197,14 @@ message AddOrganizationRequest{ } ]; repeated Admin admins = 2; + // optionally set your own id unique for the organization. + optional string org_id = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; } message AddOrganizationResponse{ diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 90c29ca354..e303b676d7 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -160,6 +160,14 @@ message AddOrganizationRequest{ } ]; repeated Admin admins = 2; + // optionally set your own id unique for the organization. + optional string org_id = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; } message AddOrganizationResponse{