diff --git a/cmd/start/start.go b/cmd/start/start.go index 291b6c4d981..a29d7714c72 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -522,7 +522,7 @@ func startAPIs( if err := apis.RegisterService(ctx, settings_v2.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, org_v2.CreateServer(commands, queries, permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, org_v2.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, feature_v2.CreateServer(commands, queries)); err != nil { diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index f11d9efc224..29f90f57b4c 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -366,7 +366,7 @@ module.exports = { }, org_v2: { specPath: - ".artifacts/openapi/zitadel/org/v2/org_service.swagger.json", + ".artifacts/openapi3/zitadel/org/v2/org_service.openapi.yaml", outputDir: "docs/apis/resources/org_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -382,15 +382,6 @@ module.exports = { categoryLinkSource: "auto", }, }, - org_v2beta: { - specPath: - ".artifacts/openapi3/zitadel/org/v2beta/org_service.openapi.yaml", - outputDir: "docs/apis/resources/org_service_v2beta", - sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", - }, - }, project_v2: { specPath: ".artifacts/openapi3/zitadel/project/v2/project_service.openapi.yaml", diff --git a/docs/sidebars.js b/docs/sidebars.js index 3f53ec336ba..3b89b8427cd 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -11,7 +11,6 @@ const sidebar_api_saml_service_v2 = require("./docs/apis/resources/saml_service_ const sidebar_api_settings_service_v2 = require("./docs/apis/resources/settings_service_v2/sidebar.ts").default const sidebar_api_feature_service_v2 = require("./docs/apis/resources/feature_service_v2/sidebar.ts").default const sidebar_api_org_service_v2 = require("./docs/apis/resources/org_service_v2/sidebar.ts").default -const sidebar_api_org_service_v2beta = require("./docs/apis/resources/org_service_v2beta/sidebar.ts").default const sidebar_api_idp_service_v2 = require("./docs/apis/resources/idp_service_v2/sidebar.ts").default const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/sidebar.ts").default const sidebar_api_project_service_v2 = require("./docs/apis/resources/project_service_v2/sidebar.ts").default @@ -764,18 +763,6 @@ module.exports = { }, items: sidebar_api_org_service_v2, }, - { - type: "category", - label: "Organization (Beta)", - link: { - type: "generated-index", - title: "Organization Service Beta API", - slug: "/apis/resources/org_service/v2beta", - description: - "This beta API is intended to manage organizations for ZITADEL. Expect breaking changes to occur. Please use the v2 version for a stable API. \n", - }, - items: sidebar_api_org_service_v2beta, - }, { type: "category", label: "Identity Provider", diff --git a/internal/api/grpc/metadata/v2/metadata.go b/internal/api/grpc/metadata/v2/metadata.go index f50ad57f643..be10beec28d 100644 --- a/internal/api/grpc/metadata/v2/metadata.go +++ b/internal/api/grpc/metadata/v2/metadata.go @@ -45,3 +45,40 @@ func UserMetadataFilterToQuery(filter *meta_pb.MetadataSearchFilter) (query.Sear return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") } } + +func OrgMetadataListToPb(dataList []*query.OrgMetadata) []*meta_pb.Metadata { + mds := make([]*meta_pb.Metadata, len(dataList)) + for i, data := range dataList { + mds[i] = OrgMetadataToPb(data) + } + return mds +} + +func OrgMetadataToPb(data *query.OrgMetadata) *meta_pb.Metadata { + return &meta_pb.Metadata{ + Key: data.Key, + Value: data.Value, + CreationDate: timestamppb.New(data.CreationDate), + ChangeDate: timestamppb.New(data.ChangeDate), + } +} + +func OrgMetadataQueriesToQuery(queries []*meta_pb.MetadataSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgMetadataFilterToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgMetadataFilterToQuery(filter *meta_pb.MetadataSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *meta_pb.MetadataSearchFilter_KeyFilter: + return query.NewOrgMetadataKeySearchQuery(q.KeyFilter.Key, filter_v2.TextMethodPbToQuery(q.KeyFilter.Method)) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") + } +} 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 1f4b6585c82..96ad7d5fbdd 100644 --- a/internal/api/grpc/org/v2/integration_test/org_test.go +++ b/internal/api/grpc/org/v2/integration_test/org_test.go @@ -4,7 +4,10 @@ package org_test import ( "context" + "errors" "os" + "slices" + "strings" "testing" "time" @@ -13,15 +16,17 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/org/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( - CTX, OwnerCTX, UserCTX context.Context - Instance *integration.Instance - Client org.OrganizationServiceClient - User *user.AddHumanUserResponse + CTX context.Context + Instance *integration.Instance + Client org.OrganizationServiceClient + User *user.AddHumanUserResponse + OtherOrganization *org.AddOrganizationResponse ) func TestMain(m *testing.M) { @@ -32,10 +37,9 @@ func TestMain(m *testing.M) { Instance = integration.NewInstance(ctx) Client = Instance.Client.OrgV2 - CTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) - OwnerCTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) - UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission) + CTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeIAMOwner) User = Instance.CreateHumanUser(CTX) + OtherOrganization = Instance.CreateOrganization(CTX, integration.OrganizationName(), integration.Email()) return m.Run() }()) } @@ -53,7 +57,7 @@ func TestServer_AddOrganization(t *testing.T) { }{ { name: "missing permission", - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: Instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &org.AddOrganizationRequest{ Name: "name", Admins: nil, @@ -218,3 +222,1101 @@ func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse assert.Empty(t, got.GetPhoneCode()) } } + +func TestServer_UpdateOrganization(t *testing.T) { + orgs, orgsName, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + orgName := orgsName[0] + + tests := []struct { + name string + ctx context.Context + req *org.UpdateOrganizationRequest + want *org.UpdateOrganizationResponse + wantErr bool + }{ + { + name: "update org with new name", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.UpdateOrganizationRequest{ + OrganizationId: orgId, + Name: "new org name", + }, + }, + { + name: "update org with same name", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.UpdateOrganizationRequest{ + OrganizationId: orgId, + Name: orgName, + }, + }, + { + name: "update org with non existent org id", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.UpdateOrganizationRequest{ + OrganizationId: "non existant org id", + // Name: "", + }, + wantErr: true, + }, + { + name: "update org with no id", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.UpdateOrganizationRequest{ + OrganizationId: "", + Name: orgName, + }, + wantErr: true, + }, + { + name: "no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &org.UpdateOrganizationRequest{ + OrganizationId: OtherOrganization.GetOrganizationId(), + Name: integration.OrganizationName(), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.UpdateOrganization(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + }) + } +} + +func TestServer_DeleteOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + createOrgFunc func() string + req *org.DeleteOrganizationRequest + want *org.DeleteOrganizationResponse + dontCheckTime bool + err error + }{ + { + name: "delete org no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + createOrgFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + return orgs[0].OrganizationId + }, + req: &org.DeleteOrganizationRequest{}, + err: errors.New("membership not found"), + }, + { + name: "delete org happy path", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + return orgs[0].OrganizationId + }, + req: &org.DeleteOrganizationRequest{}, + }, + { + name: "delete already deleted org", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + // delete org + _, err := Client.DeleteOrganization(CTX, &org.DeleteOrganizationRequest{OrganizationId: orgs[0].OrganizationId}) + require.NoError(t, err) + + return orgs[0].OrganizationId + }, + req: &org.DeleteOrganizationRequest{}, + dontCheckTime: true, + }, + { + name: "delete non existent org", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.DeleteOrganizationRequest{ + OrganizationId: "non existent org id", + }, + dontCheckTime: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.createOrgFunc != nil { + tt.req.OrganizationId = tt.createOrgFunc() + } + + got, err := Client.DeleteOrganization(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetDeletionDate().AsTime() + if !tt.dontCheckTime { + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + } + }) + } +} + +func TestServer_DeactivateReactivateNonExistentOrganization(t *testing.T) { + ctx := Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + // deactivate non existent organization + _, err := Client.DeactivateOrganization(ctx, &org.DeactivateOrganizationRequest{ + OrganizationId: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") + + // reactivate non existent organization + _, err = Client.ActivateOrganization(ctx, &org.ActivateOrganizationRequest{ + OrganizationId: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") +} + +func TestServer_ActivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Activate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &org.DeactivateOrganizationRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + { + Query: &org.SearchQuery_IdQuery{ + IdQuery: &org.OrganizationIDQuery{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + if assert.GreaterOrEqual(ttt, len(listOrgRes.Result), 1) { + require.Equal(ttt, org.OrganizationState_ORGANIZATION_STATE_INACTIVE, listOrgRes.Result[0].State) + } + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "Activate, no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + testFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + return orgId + }, + err: errors.New("membership not found"), + }, + { + name: "Activate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Activate, already activated", + ctx: CTX, + testFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + return orgId + }, + err: errors.New("Organisation is already active"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + if tt.testFunc != nil { + orgId = tt.testFunc() + } + _, err := Client.ActivateOrganization(tt.ctx, &org.ActivateOrganizationRequest{ + OrganizationId: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_DeactivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Deactivate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + return orgId + }, + }, + { + name: "Deactivate, no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + testFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + return orgId + }, + err: errors.New("membership not found"), + }, + { + name: "Deactivate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Deactivate, already deactivated", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &org.DeactivateOrganizationRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + { + Query: &org.SearchQuery_IdQuery{ + IdQuery: &org.OrganizationIDQuery{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, org.OrganizationState_ORGANIZATION_STATE_INACTIVE, listOrgRes.Result[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + err: errors.New("Organisation is already deactivated"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + orgId = tt.testFunc() + _, err := Client.DeactivateOrganization(tt.ctx, &org.DeactivateOrganizationRequest{ + OrganizationId: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_AddOrganizationDomain(t *testing.T) { + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "add org domain, happy path", + ctx: CTX, + domain: integration.DomainName(), + testFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + return orgId + }, + }, + { + name: "no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + domain: integration.DomainName(), + testFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + return orgId + }, + err: errors.New("membership not found"), + }, + { + name: "add org domain, twice", + ctx: CTX, + domain: integration.DomainName(), + testFunc: func() string { + t.Helper() + // 1. create organization + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + domain := integration.DomainName() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(ttt, err) + found := false + for _, res := range queryRes.Domains { + if res.Domain == domain { + found = true + } + } + require.True(ttt, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "add org domain to non existent org", + ctx: CTX, + domain: integration.DomainName(), + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + orgId := tt.testFunc() + addOrgDomainRes, err := Client.AddOrganizationDomain(tt.ctx, &org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + } + }) + } +} + +func TestServer_AddOrganizationDomain_ClaimDomain(t *testing.T) { + domain := integration.DomainName() + + // create an organization, ensure it has globally unique usernames + // and create a user with a loginname that matches the domain later on + organization, err := Client.AddOrganization(CTX, &org.AddOrganizationRequest{ + Name: integration.OrganizationName(), + }) + require.NoError(t, err) + _, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{ + OrgId: organization.GetOrganizationId(), + UserLoginMustBeDomain: false, + }) + require.NoError(t, err) + username := integration.Username() + "@" + domain + ownUser := Instance.CreateHumanUserVerified(CTX, organization.GetOrganizationId(), username, "") + + // create another organization, ensure it has globally unique usernames + // and create a user with a loginname that matches the domain later on + otherOrg, err := Client.AddOrganization(CTX, &org.AddOrganizationRequest{ + Name: integration.OrganizationName(), + }) + require.NoError(t, err) + _, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{ + OrgId: otherOrg.GetOrganizationId(), + UserLoginMustBeDomain: false, + }) + require.NoError(t, err) + + otherUsername := integration.Username() + "@" + domain + otherUser := Instance.CreateHumanUserVerified(CTX, otherOrg.GetOrganizationId(), otherUsername, "") + + // if we add the domain now to the first organization, it should be claimed on the second organization, resp. its user(s) + _, err = Client.AddOrganizationDomain(CTX, &org.AddOrganizationDomainRequest{ + OrganizationId: organization.GetOrganizationId(), + Domain: domain, + }) + require.NoError(t, err) + + require.EventuallyWithT(t, func(collect *assert.CollectT) { + // check both users: the first one must be untouched, the second one must be updated + users, err := Instance.Client.UserV2.ListUsers(CTX, &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{UserIds: []string{ownUser.GetUserId(), otherUser.GetUserId()}}, + }, + }, + }, + }) + require.NoError(collect, err) + require.Len(collect, users.GetResult(), 2) + + for _, u := range users.GetResult() { + if u.GetUserId() == ownUser.GetUserId() { + assert.Equal(collect, username, u.GetPreferredLoginName()) + continue + } + if u.GetUserId() == otherUser.GetUserId() { + assert.NotEqual(collect, otherUsername, u.GetPreferredLoginName()) + assert.Contains(collect, u.GetPreferredLoginName(), "@temporary.") + } + } + }, 5*time.Second, time.Second, "user not updated in time") +} + +func TestServer_DeleteOrganizationDomain(t *testing.T) { + domain := integration.DomainName() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "delete org domain, happy path", + ctx: CTX, + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(ttt, err) + + found := slices.ContainsFunc(queryRes.Domains, func(d *org.Domain) bool { return d.GetDomain() == domain }) + require.True(ttt, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "delete org domain, twice", + ctx: CTX, + domain: integration.DomainName(), + testFunc: func() string { + // 1. create organization + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + domain := integration.DomainName() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(ttt, err) + found := false + for _, res := range queryRes.Domains { + if res.Domain == domain { + found = true + } + } + require.True(ttt, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + _, err = Client.DeleteOrganizationDomain(CTX, &org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + return orgId + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "delete org domain to non existent org", + ctx: CTX, + domain: integration.DomainName(), + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "delete org domain no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(ttt, err) + + found := slices.ContainsFunc(queryRes.Domains, func(d *org.Domain) bool { return d.GetDomain() == domain }) + require.True(ttt, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + err: errors.New("membership not found"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + orgId := tt.testFunc() + + _, err := Client.DeleteOrganizationDomain(tt.ctx, &org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_ValidateOrganizationDomain(t *testing.T) { + t.Cleanup(func() { + _, err := Instance.Client.Admin.UpdateDomainPolicy(CTX, &admin.UpdateDomainPolicyRequest{ + ValidateOrgDomains: false, + }) + require.NoError(t, err) + }) + + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + _, err := Instance.Client.Admin.UpdateDomainPolicy(CTX, &admin.UpdateDomainPolicyRequest{ + ValidateOrgDomains: true, + }) + if err != nil && !strings.Contains(err.Error(), "Organisation is already deactivated") { + require.NoError(t, err) + } + + domain := integration.DomainName() + _, err = Client.AddOrganizationDomain(CTX, &org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + req *org.GenerateOrganizationDomainValidationRequest + err error + }{ + { + name: "validate org http happy path", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + }, + { + name: "validate org http non existent org id", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org dns happy path", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + }, + { + name: "validate org dns non existent org id", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org non existent domain", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + req: &org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: "non existent domain", + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate without permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("membership not found"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GenerateOrganizationDomainValidation(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + require.NotEmpty(t, got.Token) + require.Contains(t, got.Url, domain) + }) + } +} + +func TestServer_SetOrganizationMetadata(t *testing.T) { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + key string + value string + err error + }{ + { + name: "no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + orgId: orgId, + key: "key1", + value: "value1", + err: errors.New("membership not found"), + }, + { + name: "set org metadata", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + key: "key1", + value: "value1", + }, + { + name: "set org metadata on non existant org", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + orgId: "non existant orgid", + key: "key2", + value: "value2", + err: errors.New("Organisation not found"), + }, + { + name: "update org metadata", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*org.Metadata{ + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key4", + value: "value4", + }, + { + name: "update org metadata with same value", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*org.Metadata{ + { + Key: "key5", + Value: []byte("value5"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key5", + value: "value5", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + got, err := Client.SetOrganizationMetadata(tt.ctx, &org.SetOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Metadata: []*org.Metadata{ + { + Key: tt.key, + Value: []byte(tt.value), + }, + }, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetSetDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata + listMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }) + require.NoError(ttt, err) + foundMetadata := false + foundMetadataKeyCount := 0 + for _, res := range listMetadataRes.Metadata { + if res.Key == tt.key { + foundMetadataKeyCount += 1 + } + if res.Key == tt.key && + string(res.Value) == tt.value { + foundMetadata = true + } + } + require.True(ttt, foundMetadata, "unable to find added metadata") + require.Equal(ttt, 1, foundMetadataKeyCount, "same metadata key found multiple times") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganizationMetadata(t *testing.T) { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + _, err := Client.SetOrganizationMetadata(CTX, &org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, { + + Key: "key4", + Value: []byte("value4"), + }, + }, + }) + require.NoError(t, err) + + // check metadata exists + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 1*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgMetadataRes, err := Client.ListOrganizationMetadata(CTX, &org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }) + require.NoError(ttt, err) + require.Len(ttt, listOrgMetadataRes.GetMetadata(), 4) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + metadataToDelete []struct { + key string + value string + } + err error + }{ + { + name: "delete org metadata happy path", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "delete multiple org metadata happy path", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + }, + }, + { + name: "delete org metadata that does not exist", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key5", + value: "value5", + }, + }, + err: errors.New("One or more keys do not exist"), + }, + { + name: "delete org metadata for org that does not exist", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + orgId: "non existant org id", + metadataToDelete: []struct{ key, value string }{ + { + key: "key4", + value: "value4", + }, + }, + err: errors.New("Organisation not found"), + }, + { + name: "delete org metadata without permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key4", + value: "value4", + }, + }, + err: errors.New("membership not found"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + keys := make([]string, len(tt.metadataToDelete)) + for i, kvp := range tt.metadataToDelete { + keys[i] = kvp.key + } + + // run delete + _, err := Client.DeleteOrganizationMetadata(tt.ctx, &org.DeleteOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Keys: keys, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata was definitely deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, foundMetadataCount, 0) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func createOrgs(ctx context.Context, t *testing.T, client org.OrganizationServiceClient, noOfOrgs int) ([]*org.AddOrganizationResponse, []string, []string) { + var err error + orgs := make([]*org.AddOrganizationResponse, noOfOrgs) + orgNames := make([]string, noOfOrgs) + orgDomains := make([]string, noOfOrgs) + + for i := range noOfOrgs { + orgName := integration.OrganizationName() + orgNames[i] = orgName + orgs[i], err = client.AddOrganization(ctx, + &org.AddOrganizationRequest{ + Name: orgName, + }, + ) + require.NoError(t, err) + } + + for i := range noOfOrgs { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + listOrgRes, err := client.ListOrganizations(ctx, &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + { + Query: &org.SearchQuery_IdQuery{ + IdQuery: &org.OrganizationIDQuery{ + Id: orgs[i].GetOrganizationId(), + }, + }, + }, + }, + }) + require.NoError(collect, err) + require.Len(collect, listOrgRes.Result, 1) + + orgDomains[i] = listOrgRes.Result[0].PrimaryDomain + }, retryDuration, tick, "timeout waiting for org creation") + } + + return orgs, orgNames, orgDomains +} 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 d7acf45121f..5ab2fcee4af 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -4,471 +4,352 @@ package org_test import ( "context" - "slices" - "strconv" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2" "github.com/zitadel/zitadel/pkg/grpc/object/v2" + object_v2 "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/org/v2" ) -type orgAttr struct { - ID string - Name string - Details *object.Details -} - -func createOrganization(ctx context.Context, name string) orgAttr { - orgResp := Instance.CreateOrganization(ctx, name, integration.Email()) - orgResp.Details.CreationDate = orgResp.Details.ChangeDate - return orgAttr{ - ID: orgResp.GetOrganizationId(), - Name: name, - Details: orgResp.GetDetails(), - } -} - -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 - req *org.ListOrganizationsRequest - dep func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) - } + ListOrgIinstance := integration.NewInstance(CTX) + listOrgIAmOwnerCtx := ListOrgIinstance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + listOrgClient := ListOrgIinstance.Client.OrgV2 + + noOfOrgs := 3 + orgs, orgsName, orgsDomain := createOrgs(listOrgIAmOwnerCtx, t, listOrgClient, noOfOrgs) + + // deactivat org[1] + _, err := listOrgClient.DeactivateOrganization(listOrgIAmOwnerCtx, &org.DeactivateOrganizationRequest{ + OrganizationId: orgs[1].OrganizationId, + }) + require.NoError(t, err) + tests := []struct { - name string - args args - want *org.ListOrganizationsResponse - wantErr bool + name string + ctx context.Context + query []*org.SearchQuery + want *org.ListOrganizationsResponse + err error }{ { - name: "list org by default, ok", - args: args{ - CTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - DefaultOrganizationQuery(), - }, - }, - nil, - }, + name: "list organizations, without required permissions", + ctx: ListOrgIinstance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), + Details: &object_v2.ListDetails{ + TotalResult: 4, + }, + }, + }, + { + name: "list organizations happy path, no filter", + ctx: listOrgIAmOwnerCtx, + want: &org.ListOrganizationsResponse{ + Details: &object_v2.ListDetails{ + TotalResult: 4, }, - SortingColumn: 0, Result: []*org.Organization{ { - Id: Instance.DefaultOrg.Id, - Name: Instance.DefaultOrg.Name, - PrimaryDomain: Instance.DefaultOrg.PrimaryDomain, - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - Details: &object.Details{ - Sequence: Instance.DefaultOrg.Details.Sequence, - CreationDate: Instance.DefaultOrg.Details.CreationDate, - ChangeDate: Instance.DefaultOrg.Details.ChangeDate, - ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, + Id: ListOrgIinstance.DefaultOrg.Id, + Name: ListOrgIinstance.DefaultOrg.Name, + Details: &object_v2.Details{ + Sequence: ListOrgIinstance.DefaultOrg.GetDetails().GetSequence(), + ChangeDate: ListOrgIinstance.DefaultOrg.GetDetails().GetChangeDate(), + ResourceOwner: ListOrgIinstance.DefaultOrg.GetDetails().GetResourceOwner(), + CreationDate: ListOrgIinstance.DefaultOrg.GetDetails().GetCreationDate(), }, }, + { + Id: orgs[0].OrganizationId, + Name: orgsName[0], + Details: orgs[0].GetDetails(), + }, + { + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), + }, + { + Id: orgs[2].OrganizationId, + Name: orgsName[2], + Details: orgs[2].GetDetails(), + }, }, }, }, { - name: "list org by id, ok, multiple", - args: args{ - CTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - OrganizationIdQuery(Instance.DefaultOrg.Id), + name: "list organizations by id happy path", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_IdQuery{ + IdQuery: &org.OrganizationIDQuery{ + Id: orgs[1].OrganizationId, + }, }, - SortingColumn: org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME, - }, - func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { - count := 3 - orgs := make([]orgAttr, count) - prefix := integration.OrganizationName() - for i := 0; i < count; i++ { - name := prefix + strconv.Itoa(i) - orgs[i] = createOrganization(ctx, name) - } - request.Queries = []*org.SearchQuery{ - OrganizationNamePrefixQuery(prefix), - } - - slices.SortFunc(orgs, func(a, b orgAttr) int { - return -1 * strings.Compare(a.Name, b.Name) - }) - return orgs, nil }, }, want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ + Details: &object_v2.ListDetails{ + TotalResult: 1, + }, + Result: []*org.Organization{ + { + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), + }, + }, + }, + }, + { + name: "list organizations by state active", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_StateQuery{ + StateQuery: &org.OrganizationStateQuery{ + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + }, + }, + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object_v2.ListDetails{ TotalResult: 3, - Timestamp: timestamppb.Now(), }, - SortingColumn: 0, Result: []*org.Organization{ { - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - }, - { - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - }, - { - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - }, - }, - }, - }, - { - name: "list org by id, ok", - args: args{ - CTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - OrganizationIdQuery(Instance.DefaultOrg.Id), - }, - }, - nil, - }, - want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*org.Organization{ - { - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - Name: Instance.DefaultOrg.Name, - Details: &object.Details{ - Sequence: Instance.DefaultOrg.Details.Sequence, - CreationDate: Instance.DefaultOrg.Details.CreationDate, - ChangeDate: Instance.DefaultOrg.Details.ChangeDate, - ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, + Id: ListOrgIinstance.DefaultOrg.Id, + Name: ListOrgIinstance.DefaultOrg.Name, + Details: &object_v2.Details{ + Sequence: ListOrgIinstance.DefaultOrg.GetDetails().GetSequence(), + ChangeDate: ListOrgIinstance.DefaultOrg.GetDetails().GetChangeDate(), + ResourceOwner: ListOrgIinstance.DefaultOrg.GetDetails().GetResourceOwner(), + CreationDate: ListOrgIinstance.DefaultOrg.GetDetails().GetCreationDate(), }, - Id: Instance.DefaultOrg.Id, - PrimaryDomain: Instance.DefaultOrg.PrimaryDomain, + }, + { + Id: orgs[0].OrganizationId, + Name: orgsName[0], + Details: orgs[0].GetDetails(), + }, + { + Id: orgs[2].OrganizationId, + Name: orgsName[2], + Details: orgs[2].GetDetails(), }, }, }, }, { - 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 := integration.OrganizationName() - orgID := integration.ID() - 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{ - CTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - OrganizationNameQuery(Instance.DefaultOrg.Name), - }, - }, - nil, - }, - want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*org.Organization{ - { - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - Name: Instance.DefaultOrg.Name, - Details: &object.Details{ - Sequence: Instance.DefaultOrg.Details.Sequence, - CreationDate: Instance.DefaultOrg.Details.CreationDate, - ChangeDate: Instance.DefaultOrg.Details.ChangeDate, - ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, + name: "list organizations by state inactive", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_StateQuery{ + StateQuery: &org.OrganizationStateQuery{ + State: org.OrganizationState_ORGANIZATION_STATE_INACTIVE, }, - Id: Instance.DefaultOrg.Id, - PrimaryDomain: Instance.DefaultOrg.PrimaryDomain, + }, + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object_v2.ListDetails{ + TotalResult: 1, + }, + Result: []*org.Organization{ + { + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), }, }, }, }, { - name: "list org by domain, ok", - args: args{ - CTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - OrganizationDomainQuery(Instance.DefaultOrg.PrimaryDomain), - }, - }, - nil, - }, - want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*org.Organization{ - { - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - Name: Instance.DefaultOrg.Name, - Details: &object.Details{ - Sequence: Instance.DefaultOrg.Details.Sequence, - CreationDate: Instance.DefaultOrg.Details.CreationDate, - ChangeDate: Instance.DefaultOrg.Details.ChangeDate, - ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, + name: "list organizations by id bad id", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_IdQuery{ + IdQuery: &org.OrganizationIDQuery{ + Id: "bad id", }, - Id: Instance.DefaultOrg.Id, - PrimaryDomain: Instance.DefaultOrg.PrimaryDomain, }, }, }, - }, - { - name: "list org by domain (non primary), ok", - args: args{ - CTX, - &org.ListOrganizationsRequest{}, - func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { - orgs := make([]orgAttr, 1) - orgs[0] = createOrganization(ctx, integration.OrganizationName()) - domain := integration.DomainName() - _, err := Instance.Client.Mgmt.AddOrgDomain(integration.SetOrgID(ctx, orgs[0].ID), &management.AddOrgDomainRequest{ - Domain: domain, - }) - if err != nil { - return nil, err - } - request.Queries = []*org.SearchQuery{ - OrganizationDomainQuery(domain), - } - 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 inactive state, ok", - args: args{ - CTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{}, - }, - func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { - name := integration.OrganizationName() - orgResp := createOrganization(ctx, name) - deactivateOrgResp := Instance.DeactivateOrganization(ctx, orgResp.ID) - request.Queries = []*org.SearchQuery{ - OrganizationIdQuery(orgResp.ID), - OrganizationStateQuery(org.OrganizationState_ORGANIZATION_STATE_INACTIVE), - } - return []orgAttr{{ - ID: orgResp.ID, - Name: name, - Details: &object.Details{ - ResourceOwner: deactivateOrgResp.GetDetails().GetResourceOwner(), - Sequence: deactivateOrgResp.GetDetails().GetSequence(), - CreationDate: orgResp.Details.GetCreationDate(), - ChangeDate: deactivateOrgResp.GetDetails().GetChangeDate(), - }, - }}, nil - }, - }, - want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 0, - Result: []*org.Organization{ - { - State: org.OrganizationState_ORGANIZATION_STATE_INACTIVE, - Details: &object.Details{}, - }, - }, - }, - }, - { - name: "list org by domain, ok, sorted", - args: args{ - CTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - OrganizationDomainQuery(Instance.DefaultOrg.PrimaryDomain), - }, - SortingColumn: org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME, - }, - nil, - }, - want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 1, - Result: []*org.Organization{ - { - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - Name: Instance.DefaultOrg.Name, - Details: &object.Details{ - Sequence: Instance.DefaultOrg.Details.Sequence, - CreationDate: Instance.DefaultOrg.Details.ChangeDate, - ChangeDate: Instance.DefaultOrg.Details.ChangeDate, - ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, - }, - Id: Instance.DefaultOrg.Id, - PrimaryDomain: Instance.DefaultOrg.PrimaryDomain, - }, - }, - }, - }, - { - name: "list org, no result", - args: args{ - CTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - OrganizationDomainQuery("notexisting"), - }, - }, - nil, - }, want: &org.ListOrganizationsResponse{ Details: &object.ListDetails{ TotalResult: 0, - Timestamp: timestamppb.Now(), }, - SortingColumn: 0, - Result: []*org.Organization{}, + Result: nil, }, }, { - name: "list org, no login", - args: args{ - context.Background(), - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - OrganizationDomainQuery("nopermission"), + name: "list organizations specify org name equals", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_NameQuery{ + NameQuery: &org.OrganizationNameQuery{ + Name: orgsName[1], + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, }, }, - nil, - }, - wantErr: true, - }, - { - name: "list org, no permission", - args: args{ - UserCTX, - &org.ListOrganizationsRequest{}, - nil, - }, - want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 1, - Result: []*org.Organization{}, - }, - }, - { - name: "list org, no permission org owner", - args: args{ - OwnerCTX, - &org.ListOrganizationsRequest{ - Queries: []*org.SearchQuery{ - OrganizationDomainQuery("nopermission"), - }, - }, - nil, - }, - want: &org.ListOrganizationsResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - SortingColumn: 1, - Result: []*org.Organization{}, - }, - }, - { - name: "list org, org owner", - args: args{ - OwnerCTX, - &org.ListOrganizationsRequest{}, - nil, }, want: &org.ListOrganizationsResponse{ Details: &object.ListDetails{ TotalResult: 1, - Timestamp: timestamppb.Now(), }, - SortingColumn: 1, Result: []*org.Organization{ { - State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, - Name: Instance.DefaultOrg.Name, - Details: &object.Details{ - Sequence: Instance.DefaultOrg.Details.Sequence, - CreationDate: Instance.DefaultOrg.Details.ChangeDate, - ChangeDate: Instance.DefaultOrg.Details.ChangeDate, - ResourceOwner: Instance.DefaultOrg.Details.ResourceOwner, + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), + }, + }, + }, + }, + { + name: "list organizations specify org name contains", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_NameQuery{ + NameQuery: &org.OrganizationNameQuery{ + Name: func() string { + return orgsName[1][1 : len(orgsName[1])-2] + }(), + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, }, - Id: Instance.DefaultOrg.Id, - PrimaryDomain: Instance.DefaultOrg.PrimaryDomain, + }, + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*org.Organization{ + { + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), + }, + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_NameQuery{ + NameQuery: &org.OrganizationNameQuery{ + Name: func() string { + return strings.ToUpper(orgsName[1][1 : len(orgsName[1])-2]) + }(), + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*org.Organization{ + { + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), + }, + }, + }, + }, + { + name: "list organizations specify domain name equals", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_DomainQuery{ + DomainQuery: &org.OrganizationDomainQuery{ + Domain: orgsDomain[1], + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*org.Organization{ + { + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), + }, + }, + }, + }, + { + name: "list organizations specify domain name contains", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_DomainQuery{ + DomainQuery: &org.OrganizationDomainQuery{ + Domain: orgsDomain[1][1 : len(orgsDomain[1])-2], + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*org.Organization{ + { + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), + }, + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*org.SearchQuery{ + { + Query: &org.SearchQuery_DomainQuery{ + DomainQuery: &org.OrganizationDomainQuery{ + Domain: strings.ToUpper(orgsDomain[1][1 : len(orgsDomain[1])-2]), + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*org.Organization{ + { + Id: orgs[1].OrganizationId, + Name: orgsName[1], + Details: orgs[1].GetDetails(), }, }, }, @@ -476,91 +357,348 @@ func TestServer_ListOrganizations(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - orgs, err := tt.args.dep(tt.args.ctx, tt.args.req) - require.NoError(t, err) - if len(orgs) > 0 { - for i, org := range orgs { - tt.want.Result[i].Name = org.Name - tt.want.Result[i].Id = org.ID - tt.want.Result[i].Details = org.Details - } - } - } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.ListOrganizations(tt.args.ctx, tt.args.req) - if tt.wantErr { + got, err := listOrgClient.ListOrganizations(tt.ctx, &org.ListOrganizationsRequest{ + Queries: tt.query, + Query: &object.ListQuery{ + Asc: true, + }, + SortingColumn: org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_CREATION_DATE, + }) + if tt.err != nil { + require.ErrorContains(ttt, err, tt.err.Error()) + return + } + require.NoError(ttt, err) + + integration.AssertListDetails(ttt, tt.want, got) + + require.Len(ttt, got.Result, len(tt.want.Result)) + for i, got := range got.Result { + integration.AssertDetails(t, tt.want.Result[i], got) + + assert.Equal(ttt, tt.want.Result[i].Id, got.Id) + assert.Equal(ttt, tt.want.Result[i].Name, got.Name) + } + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_ListOrganizationDomains(t *testing.T) { + domain := integration.DomainName() + + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + + var primaryDomain string + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Second) + require.EventuallyWithT(t, func(t *assert.CollectT) { + organizations, err := Client.ListOrganizations(CTX, &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + {Query: &org.SearchQuery_IdQuery{ + IdQuery: &org.OrganizationIDQuery{Id: orgId}, + }}, + }, + }) + require.NoError(t, err) + require.Len(t, organizations.GetResult(), 1) + primaryDomain = organizations.GetResult()[0].GetPrimaryDomain() + }, retryDuration, tick, "could not find primary domain") + + _, err := Client.AddOrganizationDomain(CTX, &org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + request *org.ListOrganizationDomainsRequest + } + type want struct { + response *org.ListOrganizationDomainsResponse + err bool + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "non existing organization", + args: args{ + ctx: CTX, + request: &org.ListOrganizationDomainsRequest{OrganizationId: "not-existing"}, + }, + want: want{ + response: &org.ListOrganizationDomainsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + }, + Domains: nil, + }, + }, + }, + { + name: "no permission (different organization), error", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + }, + want: want{ + err: true, + }, + }, + { + name: "list org domain, all domains", + args: args{ + ctx: CTX, + request: &org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + SortingColumn: org.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE, + }, + }, + want: want{ + response: &org.ListOrganizationDomainsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + }, + Domains: []*org.Domain{ + { + OrganizationId: orgId, + Domain: primaryDomain, + IsVerified: true, + IsPrimary: true, + ValidationType: 0, + }, + { + OrganizationId: orgId, + Domain: domain, + IsVerified: true, + IsPrimary: false, + ValidationType: 0, + }, + }, + }, + }, + }, + { + name: "list specific domain", + args: args{ + ctx: CTX, + request: &org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + Filters: []*org.DomainSearchFilter{ + {Filter: &org.DomainSearchFilter_DomainFilter{DomainFilter: &org.OrganizationDomainQuery{Domain: domain}}}, + }, + }, + }, + want: want{ + response: &org.ListOrganizationDomainsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Domains: []*org.Domain{ + { + OrganizationId: orgId, + Domain: domain, + IsVerified: true, + IsPrimary: false, + ValidationType: 0, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 5*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(tt.args.ctx, tt.args.request) + if tt.want.err { require.Error(ttt, err) return } require.NoError(ttt, err) - // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions - tt.want.Details.TotalResult = got.Details.TotalResult - // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - for i := range tt.want.Result { - // domain from result, as it is generated though the create - tt.want.Result[i].PrimaryDomain = got.Result[i].PrimaryDomain - // sequence from result, as it can be with different sequence from create - tt.want.Result[i].Details.Sequence = got.Result[i].Details.Sequence - } - - for i := range tt.want.Result { - assert.Contains(ttt, got.Result, tt.want.Result[i]) - } - } - integration.AssertListDetails(t, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected user result") + assert.Len(ttt, queryRes.Domains, int(tt.want.response.GetPagination().GetTotalResult())) + assert.EqualExportedValues(ttt, tt.want.response.GetPagination(), queryRes.GetPagination()) + assert.ElementsMatch(ttt, tt.want.response.GetDomains(), queryRes.GetDomains()) + }, retryDuration, tick, "timeout waiting for adding domain") }) } } -func DefaultOrganizationQuery() *org.SearchQuery { - return &org.SearchQuery{Query: &org.SearchQuery_DefaultQuery{ - DefaultQuery: &org.DefaultOrganizationQuery{}, - }} -} - -func OrganizationIdQuery(resourceowner string) *org.SearchQuery { - return &org.SearchQuery{Query: &org.SearchQuery_IdQuery{ - IdQuery: &org.OrganizationIDQuery{ - Id: resourceowner, +func TestServer_ListOrganizationMetadata(t *testing.T) { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].OrganizationId + setRespoonse, err := Client.SetOrganizationMetadata(CTX, &org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key2.1", + Value: []byte("value3"), + }, + { + Key: "key2.2", + Value: []byte("value4"), + }, }, - }} -} + }) + require.NoError(t, err) -func OrganizationNameQuery(name string) *org.SearchQuery { - return &org.SearchQuery{Query: &org.SearchQuery_NameQuery{ - NameQuery: &org.OrganizationNameQuery{ - Name: name, - }, - }} -} + type args struct { + ctx context.Context + request *org.ListOrganizationMetadataRequest + } + type want struct { + response *org.ListOrganizationMetadataResponse + err error + } -func OrganizationNamePrefixQuery(name string) *org.SearchQuery { - return &org.SearchQuery{Query: &org.SearchQuery_NameQuery{ - NameQuery: &org.OrganizationNameQuery{ - Name: name, - Method: object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH, + tests := []struct { + name string + args args + want want + }{ + { + name: "list org metadata happy path", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + request: &org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }, + }, + want: want{ + response: &org.ListOrganizationMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + }, + Metadata: []*metadata.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + CreationDate: setRespoonse.GetSetDate(), + ChangeDate: setRespoonse.GetSetDate(), + }, + { + Key: "key2", + Value: []byte("value2"), + CreationDate: setRespoonse.GetSetDate(), + ChangeDate: setRespoonse.GetSetDate(), + }, + { + Key: "key2.1", + Value: []byte("value3"), + CreationDate: setRespoonse.GetSetDate(), + ChangeDate: setRespoonse.GetSetDate(), + }, + { + Key: "key2.2", + Value: []byte("value4"), + CreationDate: setRespoonse.GetSetDate(), + ChangeDate: setRespoonse.GetSetDate(), + }, + }, + }, + }, }, - }} -} + { + name: "list org metadata filter key", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + request: &org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + Pagination: &filter.PaginationRequest{ + Offset: 1, + Limit: 2, + }, + Filters: []*metadata.MetadataSearchFilter{ + { + Filter: &metadata.MetadataSearchFilter_KeyFilter{ + KeyFilter: &metadata.MetadataKeyFilter{ + Key: "key2", + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH, + }, + }, + }, + }, + }, + }, + want: want{ + response: &org.ListOrganizationMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 2, + }, + Metadata: []*metadata.Metadata{ + { + Key: "key2.1", + Value: []byte("value3"), + CreationDate: setRespoonse.GetSetDate(), + ChangeDate: setRespoonse.GetSetDate(), + }, + { + Key: "key2.2", + Value: []byte("value4"), + CreationDate: setRespoonse.GetSetDate(), + ChangeDate: setRespoonse.GetSetDate(), + }, + }, + }, + }, + }, + { + name: "list org metadata for non existent org", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + request: &org.ListOrganizationMetadataRequest{ + OrganizationId: "non existent orgid", + }, + }, + want: want{ + response: &org.ListOrganizationMetadataResponse{ + Pagination: &filter.PaginationResponse{}, + }, + }, + }, + { + name: "list org metadata without permission (other organization)", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + request: &org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }, + }, + want: want{ + response: &org.ListOrganizationMetadataResponse{ + Pagination: &filter.PaginationResponse{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { -func OrganizationDomainQuery(domain string) *org.SearchQuery { - return &org.SearchQuery{Query: &org.SearchQuery_DomainQuery{ - DomainQuery: &org.OrganizationDomainQuery{ - Domain: domain, - }, - }} -} + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 1*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListOrganizationMetadata(tt.args.ctx, tt.args.request) + require.NoError(ttt, err) -func OrganizationStateQuery(state org.OrganizationState) *org.SearchQuery { - return &org.SearchQuery{Query: &org.SearchQuery_StateQuery{ - StateQuery: &org.OrganizationStateQuery{ - State: state, - }, - }} + assert.EqualExportedValues(ttt, tt.want.response, got) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } } diff --git a/internal/api/grpc/org/v2/org.go b/internal/api/grpc/org/v2/org.go index 423ff92e4a2..d103159741a 100644 --- a/internal/api/grpc/org/v2/org.go +++ b/internal/api/grpc/org/v2/org.go @@ -4,10 +4,13 @@ import ( "context" "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/api/grpc/user/v2" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/grpc/org/v2" ) @@ -24,6 +27,116 @@ func (s *Server) AddOrganization(ctx context.Context, request *connect.Request[o return createdOrganizationToPb(createdOrg) } +func (s *Server) UpdateOrganization(ctx context.Context, request *connect.Request[org.UpdateOrganizationRequest]) (*connect.Response[org.UpdateOrganizationResponse], error) { + organization, err := s.command.ChangeOrg(ctx, request.Msg.GetOrganizationId(), request.Msg.GetName(), s.command.CheckPermissionOrganizationWrite) + if err != nil { + return nil, err + } + + return connect.NewResponse(&org.UpdateOrganizationResponse{ + ChangeDate: timestamppb.New(organization.EventDate), + }), nil +} + +func (s *Server) DeleteOrganization(ctx context.Context, request *connect.Request[org.DeleteOrganizationRequest]) (*connect.Response[org.DeleteOrganizationResponse], error) { + details, err := s.command.RemoveOrg(ctx, request.Msg.GetOrganizationId(), s.command.CheckPermissionOrganizationDelete, false) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.DeleteOrganizationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) SetOrganizationMetadata(ctx context.Context, request *connect.Request[org.SetOrganizationMetadataRequest]) (*connect.Response[org.SetOrganizationMetadataResponse], error) { + result, err := s.command.BulkSetOrgMetadata(ctx, request.Msg.GetOrganizationId(), s.command.CheckPermissionOrganizationWrite, bulkSetOrgMetadataToDomain(request.Msg)...) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.SetOrganizationMetadataResponse{ + SetDate: timestamppb.New(result.EventDate), + }), nil +} + +func (s *Server) DeleteOrganizationMetadata(ctx context.Context, request *connect.Request[org.DeleteOrganizationMetadataRequest]) (*connect.Response[org.DeleteOrganizationMetadataResponse], error) { + result, err := s.command.BulkRemoveOrgMetadata(ctx, request.Msg.GetOrganizationId(), s.command.CheckPermissionOrganizationWrite, request.Msg.Keys...) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.DeleteOrganizationMetadataResponse{ + DeletionDate: timestamppb.New(result.EventDate), + }), nil +} + +func (s *Server) DeactivateOrganization(ctx context.Context, request *connect.Request[org.DeactivateOrganizationRequest]) (*connect.Response[org.DeactivateOrganizationResponse], error) { + objectDetails, err := s.command.DeactivateOrg(ctx, request.Msg.GetOrganizationId(), s.command.CheckPermissionOrganizationWrite) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.DeactivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }), nil +} + +func (s *Server) ActivateOrganization(ctx context.Context, request *connect.Request[org.ActivateOrganizationRequest]) (*connect.Response[org.ActivateOrganizationResponse], error) { + objectDetails, err := s.command.ReactivateOrg(ctx, request.Msg.GetOrganizationId(), s.command.CheckPermissionOrganizationWrite) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.ActivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }), err +} + +func (s *Server) AddOrganizationDomain(ctx context.Context, request *connect.Request[org.AddOrganizationDomainRequest]) (*connect.Response[org.AddOrganizationDomainResponse], error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Msg.GetDomain(), request.Msg.GetOrganizationId()) + if err != nil { + return nil, err + } + details, err := s.command.AddOrgDomain(ctx, request.Msg.GetOrganizationId(), request.Msg.GetDomain(), userIDs, s.command.CheckPermissionOrganizationWrite) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.AddOrganizationDomainResponse{ + CreationDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) DeleteOrganizationDomain(ctx context.Context, req *connect.Request[org.DeleteOrganizationDomainRequest]) (*connect.Response[org.DeleteOrganizationDomainResponse], error) { + details, err := s.command.RemoveOrgDomain(ctx, removeOrgDomainRequestToDomain(req.Msg), s.command.CheckPermissionOrganizationWrite) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.DeleteOrganizationDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), err +} + +func (s *Server) GenerateOrganizationDomainValidation(ctx context.Context, req *connect.Request[org.GenerateOrganizationDomainValidationRequest]) (*connect.Response[org.GenerateOrganizationDomainValidationResponse], error) { + token, url, err := s.command.GenerateOrgDomainValidation(ctx, generateOrgDomainValidationRequestToDomain(req.Msg), s.command.CheckPermissionOrganizationWrite) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.GenerateOrganizationDomainValidationResponse{ + Token: token, + Url: url, + }), nil +} + +func (s *Server) VerifyOrganizationDomain(ctx context.Context, request *connect.Request[org.VerifyOrganizationDomainRequest]) (*connect.Response[org.VerifyOrganizationDomainResponse], error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Msg.GetDomain(), request.Msg.GetOrganizationId()) + if err != nil { + return nil, err + } + details, err := s.command.ValidateOrgDomain(ctx, validateOrgDomainRequestToDomain(request.Msg), userIDs, s.command.CheckPermissionOrganizationWrite) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.VerifyOrganizationDomainResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }), nil +} + func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) { admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins()) if err != nil { @@ -88,3 +201,59 @@ func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *connect.Respons CreatedAdmins: admins, }), nil } + +func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) { + return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, orgID) +} + +func bulkSetOrgMetadataToDomain(req *org.SetOrganizationMetadataRequest) []*domain.Metadata { + metadata := make([]*domain.Metadata, len(req.Metadata)) + for i, data := range req.Metadata { + metadata[i] = &domain.Metadata{ + Key: data.Key, + Value: data.Value, + } + } + return metadata +} + +func removeOrgDomainRequestToDomain(req *org.DeleteOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func generateOrgDomainValidationRequestToDomain(req *org.GenerateOrganizationDomainValidationRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + ValidationType: domainValidationTypeToDomain(req.Type), + } +} + +func domainValidationTypeToDomain(validationType org.DomainValidationType) domain.OrgDomainValidationType { + switch validationType { + case org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP: + return domain.OrgDomainValidationTypeHTTP + case org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS: + return domain.OrgDomainValidationTypeDNS + case org.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED: + return domain.OrgDomainValidationTypeUnspecified + default: + return domain.OrgDomainValidationTypeUnspecified + } +} + +func validateOrgDomainRequestToDomain(req *org.VerifyOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} diff --git a/internal/api/grpc/org/v2/query.go b/internal/api/grpc/org/v2/query.go index 09e2534e8dc..c04c8213668 100644 --- a/internal/api/grpc/org/v2/query.go +++ b/internal/api/grpc/org/v2/query.go @@ -6,10 +6,14 @@ import ( "connectrpc.com/connect" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" "github.com/zitadel/zitadel/pkg/grpc/org/v2" ) @@ -28,6 +32,81 @@ func (s *Server) ListOrganizations(ctx context.Context, req *connect.Request[org }), nil } +func (s *Server) ListOrganizationMetadata(ctx context.Context, request *connect.Request[org.ListOrganizationMetadataRequest]) (*connect.Response[org.ListOrganizationMetadataResponse], error) { + metadataQueries, err := listOrgMetadataToDomain(s.systemDefaults, request.Msg) + if err != nil { + return nil, err + } + res, err := s.query.SearchOrgMetadata(ctx, true, request.Msg.GetOrganizationId(), metadataQueries, false, true) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.ListOrganizationMetadataResponse{ + Metadata: metadata.OrgMetadataListToPb(res.Metadata), + Pagination: &filter_pb.PaginationResponse{ + TotalResult: res.Count, + AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()), + }, + }), nil +} + +func (s *Server) ListOrganizationDomains(ctx context.Context, req *connect.Request[org.ListOrganizationDomainsRequest]) (*connect.Response[org.ListOrganizationDomainsResponse], error) { + queries, err := listOrgDomainsRequestToDomain(s.systemDefaults, req.Msg) + if err != nil { + return nil, err + } + orgIDQuery, err := query.NewOrgDomainOrgIDSearchQuery(req.Msg.GetOrganizationId()) + if err != nil { + return nil, err + } + queries.Queries = append(queries.Queries, orgIDQuery) + + domains, err := s.query.SearchOrgDomains(ctx, queries, false, true) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.ListOrganizationDomainsResponse{ + Domains: domainsToPb(domains.Domains), + Pagination: &filter_pb.PaginationResponse{ + TotalResult: domains.Count, + AppliedLimit: uint64(req.Msg.GetPagination().GetLimit()), + }, + }), nil +} + +func listOrgDomainsRequestToDomain(systemDefaults systemdefaults.SystemDefaults, request *org.ListOrganizationDomainsRequest) (*query.OrgDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := DomainQueriesToModel(request.Filters) + if err != nil { + return nil, err + } + return &query.OrgDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToOrganizationDomainColumn(request.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func fieldNameToOrganizationDomainColumn(column org.DomainFieldName) query.Column { + switch column { + case org.DomainFieldName_DOMAIN_FIELD_NAME_NAME: + return query.OrgDomainDomainCol + case org.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.OrgDomainCreationDateCol + case org.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED: + return query.Column{} + default: + return query.Column{} + } +} + func listOrgRequestToModel(ctx context.Context, req *connect.Request[org.ListOrganizationsRequest]) (*connect.Response[query.OrgSearchQueries], error) { offset, limit, asc := object.ListQueryToQuery(req.Msg.Query) queries, err := orgQueriesToQuery(ctx, req.Msg.Queries) @@ -107,6 +186,8 @@ func fieldNameToOrganizationColumn(fieldName org.OrganizationFieldName) query.Co switch fieldName { case org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME: return query.OrgColumnName + case org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_CREATION_DATE: + return query.OrgColumnCreationDate case org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_UNSPECIFIED: return query.Column{} default: @@ -136,3 +217,73 @@ func organizationToPb(organization *query.Org) *org.Organization { State: orgStateToPb(organization.State), } } + +func listOrgMetadataToDomain(systemDefaults systemdefaults.SystemDefaults, request *org.ListOrganizationMetadataRequest) (*query.OrgMetadataSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := metadata.OrgMetadataQueriesToQuery(request.GetFilters()) + if err != nil { + return nil, err + } + return &query.OrgMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil +} + +func DomainQueriesToModel(queries []*org.DomainSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = DomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func DomainQueryToModel(searchQuery *org.DomainSearchFilter) (query.SearchQuery, error) { + switch q := searchQuery.Filter.(type) { + case *org.DomainSearchFilter_DomainFilter: + return query.NewOrgDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainFilter.Method), q.DomainFilter.GetDomain()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Ags89", "List.Query.Invalid") + } +} + +func domainsToPb(domains []*query.Domain) []*org.Domain { + d := make([]*org.Domain, len(domains)) + for i, domain := range domains { + d[i] = domainToPb(domain) + } + return d +} + +func domainToPb(d *query.Domain) *org.Domain { + return &org.Domain{ + OrganizationId: d.OrgID, + Domain: d.Domain, + IsVerified: d.IsVerified, + IsPrimary: d.IsPrimary, + ValidationType: domainValidationTypeToPb(d.ValidationType), + } +} + +func domainValidationTypeToPb(validationType domain.OrgDomainValidationType) org.DomainValidationType { + switch validationType { + case domain.OrgDomainValidationTypeDNS: + return org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS + case domain.OrgDomainValidationTypeHTTP: + return org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP + case domain.OrgDomainValidationTypeUnspecified: + return org.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + default: + return org.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + } +} diff --git a/internal/api/grpc/org/v2/server.go b/internal/api/grpc/org/v2/server.go index 6fd318d1143..cfd8a03acc0 100644 --- a/internal/api/grpc/org/v2/server.go +++ b/internal/api/grpc/org/v2/server.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/org/v2" @@ -18,19 +19,20 @@ import ( var _ orgconnect.OrganizationServiceHandler = (*Server)(nil) type Server struct { + systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries checkPermission domain.PermissionCheck } -type Config struct{} - func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, checkPermission: checkPermission, diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 1a889ede9aa..e417f3a37e6 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1175,7 +1175,7 @@ service AdminService { // Get Organization By ID // - // Deprecated: use [organization service v2 ListOrganizations](apis/resources/org_service_v2/organization-service-list-organizations.api.mdx) instead. + // Deprecated: use [organization service v2 ListOrganizations](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-list-organizations.api.mdx) instead. // // Returns an organization by its ID. Make sure the user has the permissions to access the organization. rpc GetOrgByID(GetOrgByIDRequest) returns (GetOrgByIDResponse) { @@ -1201,7 +1201,7 @@ service AdminService { // Is Organization Unique // - // Deprecated: use [organization service v2 ListOrganizations](apis/resources/org_service_v2/organization-service-list-organizations.api.mdx) instead. + // Deprecated: use [organization service v2 ListOrganizations](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-list-organizations.api.mdx) instead. // // Checks if an organization with the searched parameters already exists or not. rpc IsOrgUnique(IsOrgUniqueRequest) returns (IsOrgUniqueResponse) { @@ -1250,7 +1250,7 @@ service AdminService { // Get Default Organization // - // Deprecated: use [organization service v2 ListOrganizations](apis/resources/org_service_v2/organization-service-list-organizations.api.mdx) instead. + // Deprecated: use [organization service v2 ListOrganizations](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-list-organizations.api.mdx) instead. // // Get the default organization of the ZITADEL instance. If no specific organization is given on the register form, a user will be registered to the default organization. rpc GetDefaultOrg(GetDefaultOrgRequest) returns (GetDefaultOrgResponse) { @@ -1271,7 +1271,7 @@ service AdminService { // Search Organizations // - // Deprecated: use [organization service v2 ListOrganizations](apis/resources/org_service_v2/organization-service-list-organizations.api.mdx) instead. + // Deprecated: use [organization service v2 ListOrganizations](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-list-organizations.api.mdx) instead. // // Returns a list of organizations that match the requesting filters. All filters are applied with an AND condition. rpc ListOrgs(ListOrgsRequest) returns (ListOrgsResponse) { @@ -1309,7 +1309,7 @@ service AdminService { // Setup Organization // - // Deprecated: use [organization service v2 CreateOrganization](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-create-organization.api.mdx) instead. + // Deprecated: use [organization service v2 AddOrganization](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-add-organization.api.mdx) instead. // // Create a new organization with an administrative user. If no specific roles are sent for the first user, the user will get the role ORG_OWNER. rpc SetUpOrg(SetUpOrgRequest) returns (SetUpOrgResponse) { @@ -1347,7 +1347,7 @@ service AdminService { // Remove Organization // - // Deprecated: use [organization service v2 DeleteOrganization](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-delete-organization.api.mdx) instead. + // Deprecated: use [organization service v2 DeleteOrganization](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-delete-organization.api.mdx) instead. // // Deletes the organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in. rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index d79aaa90eb8..802e8bf43ea 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2238,7 +2238,7 @@ service ManagementService { // Get Organization By Domain // - // Deprecated: use [organization v2 service ListOrganizations](apis/resources/org_service_v2/organization-service-list-organizations.api.mdx) instead. + // Deprecated: use [organization v2 service ListOrganizations](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-list-organizations.api.mdx) instead. // // Search an organization by the domain, overall organizations. The domain must match exactly. rpc GetOrgByDomainGlobal(GetOrgByDomainGlobalRequest) returns (GetOrgByDomainGlobalResponse) { @@ -2283,7 +2283,7 @@ service ManagementService { // Create Organization // - // Deprecated: use [organization service v2 CreateOrganization](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-create-organization.api.mdx) instead + // Deprecated: use [organization service v2 CreateOrganization](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-add-organization.api.mdx) instead // // Create a new organization. Based on the given name a domain will be generated to be able to identify users within an organization. rpc AddOrg(AddOrgRequest) returns (AddOrgResponse) { @@ -2312,7 +2312,7 @@ service ManagementService { // Update Organization // - // Deprecated: use [organization service v2 UpdateOrganization](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-update-organization.api.mdx) instead. + // Deprecated: use [organization service v2 UpdateOrganization](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-update-organization.api.mdx) instead. // // Change the name of the organization. rpc UpdateOrg(UpdateOrgRequest) returns (UpdateOrgResponse) { @@ -2341,7 +2341,7 @@ service ManagementService { // Deactivate Organization // - // Deprecated: use [organization service v2 DeactivateOrganization](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-deactivate-organization.api.mdx) instead. + // Deprecated: use [organization service v2 DeactivateOrganization](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-deactivate-organization.api.mdx) instead. // // Sets the state of my organization to deactivated. Users of this organization will not be able to log in. rpc DeactivateOrg(DeactivateOrgRequest) returns (DeactivateOrgResponse) { @@ -2370,7 +2370,7 @@ service ManagementService { // Reactivate Organization // - // Deprecated: use [organization service v2 ActivateOrganization](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-activate-organization.api.mdx) instead. + // Deprecated: use [organization service v2 ActivateOrganization](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-activate-organization.api.mdx) instead. // // Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again. rpc ReactivateOrg(ReactivateOrgRequest) returns (ReactivateOrgResponse) { @@ -2399,7 +2399,7 @@ service ManagementService { // Delete Organization // - // Deprecated: use [organization service v2 DeleteOrganization](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-delete-organization.api.mdx) instead. + // Deprecated: use [organization service v2 DeleteOrganization](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-delete-organization.api.mdx) instead. // // Deletes my organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in. rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { @@ -2427,7 +2427,7 @@ service ManagementService { // Set Organization Metadata // - // Deprecated: use [organization service v2 SetOrganizationMetadata](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-set-organization-metadata.api.mdx) instead. + // Deprecated: use [organization service v2 SetOrganizationMetadata](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-set-organization-metadata.api.mdx) instead. // // This endpoint either adds or updates a metadata value for the requested key. Make sure the value is base64 encoded. rpc SetOrgMetadata(SetOrgMetadataRequest) returns (SetOrgMetadataResponse) { @@ -2457,7 +2457,7 @@ service ManagementService { // Bulk Set Organization Metadata // - // Deprecated: use [organization service v2 SetOrganizationMetadata](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-set-organization-metadata.api.mdx) instead. + // Deprecated: use [organization service v2 SetOrganizationMetadata](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-set-organization-metadata.api.mdx) instead. // // This endpoint sets a list of metadata to the organization. Make sure the values are base64 encoded. rpc BulkSetOrgMetadata(BulkSetOrgMetadataRequest) returns (BulkSetOrgMetadataResponse) { @@ -2487,7 +2487,7 @@ service ManagementService { // Search Organization Metadata // - // Deprecated: use [organization service v2 ListOrganizationMetadata](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-list-organization-metadata.api.mdx) instead. + // Deprecated: use [organization service v2 ListOrganizationMetadata](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-list-organization-metadata.api.mdx) instead. // // Get the metadata of an organization filtered by your query. rpc ListOrgMetadata(ListOrgMetadataRequest) returns (ListOrgMetadataResponse) { @@ -2517,7 +2517,7 @@ service ManagementService { // Get Organization Metadata By Key // - // Deprecated: use [organization service v2 ListOrganizationMetadata](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-list-organization-metadata.api.mdx) instead. + // Deprecated: use [organization service v2 ListOrganizationMetadata](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-list-organization-metadata.api.mdx) instead. // // Get a metadata object from an organization by a specific key. rpc GetOrgMetadata(GetOrgMetadataRequest) returns (GetOrgMetadataResponse) { @@ -2546,7 +2546,7 @@ service ManagementService { // Delete Organization Metadata By Key // - // Deprecated: use [organization service v2 DeleteOrganizationMetadata](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-delete-organization-metadata.api.mdx) instead. + // Deprecated: use [organization service v2 DeleteOrganizationMetadata](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-delete-organization-metadata.api.mdx) instead. // // Remove a metadata object from an organization with a specific key. rpc RemoveOrgMetadata(RemoveOrgMetadataRequest) returns (RemoveOrgMetadataResponse) { @@ -2575,7 +2575,7 @@ service ManagementService { // Bulk Delete Metadata // - // Deprecated: use [organization service v2 DeleteOrganizationMetadata](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-delete-organization-metadata.api.mdx) instead. + // Deprecated: use [organization service v2 DeleteOrganizationMetadata](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-delete-organization-metadata.api.mdx) instead. // // Remove a list of metadata objects from an organization with a list of keys. rpc BulkRemoveOrgMetadata(BulkRemoveOrgMetadataRequest) returns (BulkRemoveOrgMetadataResponse) { @@ -2605,7 +2605,7 @@ service ManagementService { // Add Domain // - // Deprecated: use [organization service v2 AddOrganizationDomain](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-add-organization-domain.api.mdx) instead. + // Deprecated: use [organization service v2 AddOrganizationDomain](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-add-organization-domain.api.mdx) instead. // // Add a new domain to an organization. The domains are used to identify to which organization a user belongs. rpc AddOrgDomain(AddOrgDomainRequest) returns (AddOrgDomainResponse) { @@ -2634,7 +2634,7 @@ service ManagementService { // Search Domains // - // Deprecated: use [organization service v2 ListOrganizationDomains](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-list-organization-domains.api.mdx) instead. + // Deprecated: use [organization service v2 ListOrganizationDomains](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-list-organization-domains.api.mdx) instead. // // Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs. rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { @@ -2663,7 +2663,7 @@ service ManagementService { // Remove Domain // - // Deprecated: use [organization service v2 DeleteOrganizationDomain](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-delete-organization-domain.api.mdx) instead. + // Deprecated: use [organization service v2 DeleteOrganizationDomain](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-delete-organization-domain.api.mdx) instead. // // Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead. rpc RemoveOrgDomain(RemoveOrgDomainRequest) returns (RemoveOrgDomainResponse) { @@ -2691,7 +2691,7 @@ service ManagementService { // Generate Domain Verification // - // Deprecated: use [organization service v2 GenerateOrganizationDomainValidation](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-generate-organization-domain-validation.api.mdx) instead. + // Deprecated: use [organization service v2 GenerateOrganizationDomainValidation](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-generate-organization-domain-validation.api.mdx) instead. // // Generate a new file to be able to verify your domain with DNS or HTTP challenge. rpc GenerateOrgDomainValidation(GenerateOrgDomainValidationRequest) returns (GenerateOrgDomainValidationResponse) { @@ -2720,7 +2720,7 @@ service ManagementService { // Verify Domain // - // Deprecated: use [organization service v2 VerifyOrganizationDomain](apis/resources/org_service_v2beta/zitadel-org-v-2-beta-organization-service-verify-organization-domain.api.mdx) instead. + // Deprecated: use [organization service v2 VerifyOrganizationDomain](apis/resources/org_service_v2/zitadel-org-v-2-organization-service-verify-organization-domain.api.mdx) instead. // // Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique. rpc ValidateOrgDomain(ValidateOrgDomainRequest) returns (ValidateOrgDomainResponse) { diff --git a/proto/zitadel/org/v2/org.proto b/proto/zitadel/org/v2/org.proto index 1f23b85eb55..0b2602f970b 100644 --- a/proto/zitadel/org/v2/org.proto +++ b/proto/zitadel/org/v2/org.proto @@ -12,21 +12,26 @@ import "zitadel/object/v2/object.proto"; message Organization { - // Unique identifier of the organization. + // ID is the unique identifier of the organization. string id = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"69629023906488334\"" } ]; + + // Details about the organizations' creation and change date. zitadel.object.v2.Details details = 2; - // Current state of the organization, for example active, inactive and deleted. + + // Current state of the organization, for example active, inactive or deleted. OrganizationState state = 3; + // Name of the organization. string name = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"ZITADEL\""; } ]; + // Primary domain used in the organization. string primary_domain = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -40,4 +45,39 @@ enum OrganizationState { ORGANIZATION_STATE_ACTIVE = 1; ORGANIZATION_STATE_INACTIVE = 2; ORGANIZATION_STATE_REMOVED = 3; -} \ No newline at end of file +} + +enum DomainValidationType { + DOMAIN_VALIDATION_TYPE_UNSPECIFIED = 0; + // HTTP validation requires you to host a specific file on your domain. + // This file is checked by us to verify that you own the domain. + DOMAIN_VALIDATION_TYPE_HTTP = 1; + // DNS validation requires you to create a specific TXT record in your domain's DNS settings. + // This record is checked by us to verify that you own the domain. + DOMAIN_VALIDATION_TYPE_DNS = 2; +} + +message Domain { + // OrganizationID is the unique identifier of the organization the domain belongs to. + string organization_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // Domain is the fully qualified domain name. + string domain = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\""; + } + ]; + + // IsVerified is a boolean flag indicating if the domain has been verified. + bool is_verified = 3; + + // IsPrimary is a boolean flag indicating if the domain is the primary domain of the organization. + bool is_primary = 4; + + // ValidationType indicates the protocol used to validate the domain ownership. + DomainValidationType validation_type = 5; +} diff --git a/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto index 9de0aabd109..c09755e3f5e 100644 --- a/proto/zitadel/org/v2/org_service.proto +++ b/proto/zitadel/org/v2/org_service.proto @@ -14,11 +14,14 @@ import "zitadel/user/v2/user_service.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "zitadel/org/v2/org.proto"; import "zitadel/org/v2/query.proto"; +import "zitadel/filter/v2/filter.proto"; +import "zitadel/metadata/v2/metadata.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org"; @@ -110,11 +113,15 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } }; +// This service is intended to manage organizations in a ZITADEL instance. service OrganizationService { - // Create an Organization + // Add Organization // // Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER. + // + // Required permission: + // - `org.create` rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) { option (google.api.http) = { post: "/v2/organizations" @@ -140,9 +147,54 @@ service OrganizationService { }; } - // Search Organizations + + // Update Organization // - // Search for Organizations. By default, we will return all organization of the instance. Make sure to include a limit and sorting for pagination.. + // Change the name of the organization. + // + // Required permission: + // - `org.write` + rpc UpdateOrganization(UpdateOrganizationRequest) returns (UpdateOrganizationResponse) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization updated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + responses: { + key: "409" + value: { + description: "Organisation's name already taken"; + } + }; + }; + } + + // List Organizations + // + // Search for Organizations. By default, we will return all organization of the instance that you have permission to read. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `org.read` rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) { option (google.api.http) = { post: "/v2/organizations/_search"; @@ -175,6 +227,311 @@ service OrganizationService { }; }; } + + // Delete Organization + // + // Deletes the organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in. + // + // Required permission: + // - `org.delete` + rpc DeleteOrganization(DeleteOrganizationRequest) returns (DeleteOrganizationResponse) { + option (google.api.http) = { + delete: "/v2/organizations/{organization_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // Set Organization Metadata + // + // Adds or updates a metadata value for the requested key. Make sure the value is base64 encoded. + // + // Required permission: + // - `org.write` + rpc SetOrganizationMetadata(SetOrganizationMetadataRequest) returns (SetOrganizationMetadataResponse) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}/metadata" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "400" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // List Organization Metadata + // + // List metadata of an organization filtered by query. + // + // Required permission: + // - `org.read` + rpc ListOrganizationMetadata(ListOrganizationMetadataRequest) returns (ListOrganizationMetadataResponse ) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}/metadata/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Metadata + // + // Delete metadata objects from an organization with a specific key. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationMetadata(DeleteOrganizationMetadataRequest) returns (DeleteOrganizationMetadataResponse) { + option (google.api.http) = { + delete: "/v2/organizations/{organization_id}/metadata" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Add Organization Domain + // + // Add a new domain to an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.write` + rpc AddOrganizationDomain(AddOrganizationDomainRequest) returns (AddOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}/domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "409" + value: { + description: "Domain already exists"; + } + }; + }; + + } + + // List Organization Domains + // + // Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.read` + rpc ListOrganizationDomains(ListOrganizationDomainsRequest) returns (ListOrganizationDomainsResponse) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}/domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Domain + // + // Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationDomain(DeleteOrganizationDomainRequest) returns (DeleteOrganizationDomainResponse) { + option (google.api.http) = { + delete: "/v2/organizations/{organization_id}/domains" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Generate Organization Domain Validation + // + // Generate a new file to be able to verify your domain with DNS or HTTP challenge. + // + // Required permission: + // - `org.write` + rpc GenerateOrganizationDomainValidation(GenerateOrganizationDomainValidationRequest) returns (GenerateOrganizationDomainValidationResponse) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}/domains/validation/generate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "404" + value: { + description: "Domain doesn't exist on organization"; + } + }; + }; + } + + // Verify Organization Domain + // + // Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique. + // + // Required permission: + // - `org.write` + rpc VerifyOrganizationDomain(VerifyOrganizationDomainRequest) returns (VerifyOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}/domains/validation/verify" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Deactivate Organization + // + // Sets the state of my organization to deactivated. Users of this organization will not be able to log in. + // + // Required permission: + // - `org.write` + rpc DeactivateOrganization(DeactivateOrganizationRequest) returns (DeactivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Activate Organization + // + // Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again. + // + // Required permission: + // - `org.write` + rpc ActivateOrganization(ActivateOrganizationRequest) returns (ActivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2/organizations/{organization_id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } } message AddOrganizationRequest{ @@ -187,6 +544,8 @@ message AddOrganizationRequest{ repeated string roles = 3; } + // Name is the unique name of the organization to be created. + // This must be unique across the instance. string name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, @@ -196,9 +555,30 @@ message AddOrganizationRequest{ example: "\"ZITADEL\""; } ]; + + // Specify users to be assigned as organization admins. + // If no users are specified here, the organization will be created without any admin users. + // The organization can still be managed by any instance administrator. + // If no roles are specified for a user, they will be assigned the role ORG_OWNER. repeated Admin admins = 2; - // optionally set your own id unique for the organization. + + // OrganizationID is the unique identifier of the organization. This field is optional. + // If omitted, the system will generate one, + // which is the recommended way. The generated ID will be returned in the response. + optional string organization_id = 4 [ + (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\""; + } + ]; + + // Optionally, set a unique id for the organization. If omitted, the system will generate one, + // which is the recommended way. The generated ID will be returned in the response. + // + // Deprecated: use 'organization_id' field instead. optional string org_id = 3 [ + deprecated = true, (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { max_length: 200; @@ -218,17 +598,403 @@ message AddOrganizationResponse{ repeated CreatedAdmin created_admins = 3; } +message UpdateOrganizationRequest { + // OrganizationID is the unique identifier of the organization to be updated. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // Name is the new name for the organization to be set. + // Note that since the name is used to generate the organization's default domain, + // changing the name will also change the domain. + // Additionally, if the domain is used as suffix for user logins, + // their login names will also change accordingly. + // It will not affect any custom domains added to the organization. + string name = 2 [ + (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: "\"Customer 1\""; + } + ]; +} + +message UpdateOrganizationResponse { + // ChangeDate is the timestamp of the update to the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + message ListOrganizationsRequest { //list limitations and ordering zitadel.object.v2.ListQuery query = 1; + // the field the result is sorted zitadel.org.v2.OrganizationFieldName sorting_column = 2; - //criteria the client is looking for + + // criteria the client is looking for repeated zitadel.org.v2.SearchQuery queries = 3; } message ListOrganizationsResponse { + // Details of organizations results. zitadel.object.v2.ListDetails details = 1; + + // The sorting columns the result is sorted. zitadel.org.v2.OrganizationFieldName sorting_column = 2; + + // The Result is a list of organizations matching the query. repeated zitadel.org.v2.Organization result = 3; } + +message DeleteOrganizationRequest { + // OrganizationID is the unique identifier of the organization to be deleted. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeleteOrganizationResponse { + // DeletionDate is the timestamp of the deletion of the organization. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateOrganizationRequest { + // OrganizationID is the unique identifier of the organization to be deactivated. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + }, + (google.api.field_behavior) = REQUIRED + ]; +} + +message DeactivateOrganizationResponse { + // ChangeDate is the timestamp of the deactivation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateOrganizationRequest { + // OrganizationID is the unique identifier of the organization to be activated. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + }, + (google.api.field_behavior) = REQUIRED + ]; +} + +message ActivateOrganizationResponse { + // ChangeDate is the timestamp of the activation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddOrganizationDomainRequest { + // OrganizationID is the unique identifier of the organization to which the domain is to be added. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // Domain is the full qualified domain name to be added to the organization. + // Note that the domain has to be unique across the instance. + // Depending on the settings, you might have to verify the domain before it can be used. + string domain = 2 [ + (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: "\"testdomain.com\""; + } + ]; +} + +message AddOrganizationDomainResponse { + // CreationDate is the timestamp when the organization domain was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListOrganizationDomainsRequest { + // OrganizationID is the unique identifier of the organization from which the domains are to be listed. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 2; + + // Filters define the criteria to query for. + repeated DomainSearchFilter filters = 3; + + // SortingColumn is the field the result is sorted by. + // Beware that if you change this, your result pagination might be inconsistent. + DomainFieldName sorting_column = 4; +} + +message ListOrganizationDomainsResponse { + // Pagination of the organizations domain results. + zitadel.filter.v2.PaginationResponse pagination = 1; + + // Domains is a list of fully qualified domain names registered to the organization matching the query. + repeated Domain domains = 2; +} + +message DeleteOrganizationDomainRequest { + // OrganizationID is the unique identifier of the organization from which the domain is to be deleted. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // Domain is the full qualified domain name to be deleted from the organization. + // Note that if the domain is used as suffix for user logins, + // those users will not be able to log in anymore. They have to use another domain instead. + // Also if the domain was used for domain discovery, + // users will not be able to find the organization by the domain anymore. + string domain = 2 [ + (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: "\"testdomain.com\""; + } + ]; +} + +message DeleteOrganizationDomainResponse { + // DeletionDate is the timestamp of the deletion of the organization domain. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GenerateOrganizationDomainValidationRequest { + // OrganizationID is the unique identifier of the organization for which the domain validation is to be generated. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // Domain is the full qualified domain name for which the validation is to be generated. + string domain = 2 [ + (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: "\"testdomain.com\""; + } + ]; + + // Type is the domain validation type to be generated. + // Depending on the type, you will have to add a DNS record or a file to your webserver. + // Make sure that the challenge is reachable via the chosen method. + // The validation has to be done within one hour, otherwise the token will expire and you will have to generate a new one. + // After you have added the record or the file, you can verify the domain via the VerifyOrganizationDomain endpoint. + // You can check the status of the domain via the ListOrganizationDomains endpoint. + // The domain will be marked as verified after a successful verification. + DomainValidationType type = 3 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message GenerateOrganizationDomainValidationResponse { + // Token is a verification token that needs to be added to the DNS records or as a file to the webserver. + // Zitadel will check for this token to verify the domain. + string token = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; + + // URL is the location where the token needs to be placed for HTTP challenge. + string url = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://testdomain.com/.well-known/zitadel-challenge/ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; +} + +message VerifyOrganizationDomainRequest { + // Organization ID is the unique identifier of the organization whose domain is to be verified. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // Domain is the full qualified domain name to be verified. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 253; + example: "\"testdomain.com\""; + } + ]; +} + +message VerifyOrganizationDomainResponse { + // ChangeDate is the timestamp of the verification of the organization domain. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message Metadata { + // Key is identifier of the metadata entry. + string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + + // Value is the values of the metadata entry. + bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}]; +} +message SetOrganizationMetadataRequest{ + // Organization ID is the unique identifier of the organization whose metadata is to be set. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // Metadata is a list of metadata entries to set. + repeated Metadata metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; +} + +message SetOrganizationMetadataResponse{ + // The timestamp of the update of the organization metadata. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationMetadataRequest { + // Organization ID is the unique identifier of the organization whose metadata is to be listed. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 2; + + // Filters define the criteria to query the metadata for. + repeated zitadel.metadata.v2.MetadataSearchFilter filters = 3; +} + +message ListOrganizationMetadataResponse { + // Pagination of the Organizations metadata results. + zitadel.filter.v2.PaginationResponse pagination = 1; + + // Metadata is a list of organization metadata that matched the query. + repeated zitadel.metadata.v2.Metadata metadata = 2; +} + +message DeleteOrganizationMetadataRequest { + // Organization ID is the unique identifier of the organization whose metadata is to be deleted. + string organization_id = 1 [ + (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: "\"69629012906488334\""; + } + ]; + + // Keys are the organization metadata entries to be deleted by their key. + repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}]; +} + +message DeleteOrganizationMetadataResponse{ + // DeletionDate is the timestamp of the deletion of the organization metadata. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + + diff --git a/proto/zitadel/org/v2/query.proto b/proto/zitadel/org/v2/query.proto index 0606dd357e5..11996c27fbb 100644 --- a/proto/zitadel/org/v2/query.proto +++ b/proto/zitadel/org/v2/query.proto @@ -16,10 +16,19 @@ message SearchQuery { oneof query { option (validate.required) = true; + // Filter organizations by their name. OrganizationNameQuery name_query = 1; + + // Filter organizations by their domain. OrganizationDomainQuery domain_query = 2; + + // Filter organizations by their state. OrganizationStateQuery state_query = 3; + + // Filter organizations by their id. OrganizationIDQuery id_query = 4; + + // Filter for the default organization. DefaultOrganizationQuery default_query = 5; } } @@ -35,6 +44,7 @@ message OrganizationNameQuery { example: "\"gigi-giraffe\""; } ]; + // Defines which text equality method is used. zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true @@ -52,6 +62,7 @@ message OrganizationDomainQuery { example: "\"citadel.cloud\""; } ]; + // Defines which text equality method is used. zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true @@ -81,6 +92,22 @@ message OrganizationIDQuery { enum OrganizationFieldName { ORGANIZATION_FIELD_NAME_UNSPECIFIED = 0; ORGANIZATION_FIELD_NAME_NAME = 1; + ORGANIZATION_FIELD_NAME_CREATION_DATE = 2; } -message DefaultOrganizationQuery {} \ No newline at end of file +message DefaultOrganizationQuery {} + +message DomainSearchFilter { + oneof filter { + option (validate.required) = true; + + // Filter organization domains by their domain name. + OrganizationDomainQuery domain_filter = 1; + } +} + +enum DomainFieldName { + DOMAIN_FIELD_NAME_UNSPECIFIED = 0; + DOMAIN_FIELD_NAME_NAME = 1; + DOMAIN_FIELD_NAME_CREATION_DATE = 2; +} \ No newline at end of file diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 1e54d179b3a..33a6264e676 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -107,6 +107,9 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } }; +// This service is intended to manage organizations in a ZITADEL instance. +// +// Deprecated: use organization service v2 instead. This service will be removed in the next major version of ZITADEL. service OrganizationService { // Create Organization @@ -148,6 +151,8 @@ service OrganizationService { // Update Organization // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Change the name of the organization. // // Required permission: @@ -189,6 +194,8 @@ service OrganizationService { // List Organizations // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Returns a list of organizations that match the requesting filters. All filters are applied with an AND condition. // // Required permission: @@ -217,6 +224,8 @@ service OrganizationService { // Delete Organization // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Deletes the organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in. // // Required permission: @@ -250,6 +259,8 @@ service OrganizationService { // Set Organization Metadata // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Adds or updates a metadata value for the requested key. Make sure the value is base64 encoded. // // Required permission: @@ -282,6 +293,8 @@ service OrganizationService { // List Organization Metadata // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // List metadata of an organization filtered by query. // // Required permission: @@ -307,6 +320,8 @@ service OrganizationService { // Delete Organization Metadata // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Delete metadata objects from an organization with a specific key. // // Required permission: @@ -332,6 +347,8 @@ service OrganizationService { // Add Organization Domain // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Add a new domain to an organization. The domains are used to identify to which organization a user belongs. // // Required permission: @@ -364,6 +381,8 @@ service OrganizationService { // List Organization Domains // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs. // // Required permission: @@ -390,6 +409,8 @@ service OrganizationService { // Delete Organization Domain // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead. // // Required permission: @@ -415,6 +436,8 @@ service OrganizationService { // Generate Organization Domain Validation // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Generate a new file to be able to verify your domain with DNS or HTTP challenge. // // Required permission: @@ -446,6 +469,8 @@ service OrganizationService { // Verify Organization Domain // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique. // // Required permission: @@ -471,6 +496,8 @@ service OrganizationService { // Deactivate Organization // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Sets the state of my organization to deactivated. Users of this organization will not be able to log in. // // Required permission: @@ -497,6 +524,8 @@ service OrganizationService { // Activate Organization // + // Deprecated: please move to the corresponding endpoint under organization service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again. // // Required permission: