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{