diff --git a/cmd/start/start.go b/cmd/start/start.go index c712af057..c4dc4d287 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -529,7 +529,7 @@ func startAPIs( if err := apis.RegisterService(ctx, settings_v2.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); 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 557d8fd72..ac1a81c03 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 1dc6b94b8..7eff47894 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 @@ -784,18 +783,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/actions/object/metadata.go b/internal/actions/object/metadata.go index 2a80d19f8..483f109b4 100644 --- a/internal/actions/object/metadata.go +++ b/internal/actions/object/metadata.go @@ -85,6 +85,7 @@ func GetOrganizationMetadata(ctx context.Context, queries *query.Queries, c *act organizationID, &query.OrgMetadataSearchQueries{}, false, + false, ) if err != nil { logging.WithError(err).Info("unable to get org metadata in action") diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 7ce2dbd7b..b4aaa191e 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -290,7 +290,7 @@ func (s *Server) getDomains(ctx context.Context, orgID string) (_ []*org_pb.Doma if err != nil { return nil, err } - orgDomainsQuery, err := s.query.SearchOrgDomains(ctx, &query.OrgDomainSearchQueries{Queries: []query.SearchQuery{orgDomainOrgIDQuery}}, false) + orgDomainsQuery, err := s.query.SearchOrgDomains(ctx, &query.OrgDomainSearchQueries{Queries: []query.SearchQuery{orgDomainOrgIDQuery}}, false, false) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 42ff65bff..320b57186 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -885,7 +885,7 @@ func importOrgDomains(ctx context.Context, s *Server, errors *[]*admin_pb.Import Verified: domainR.IsVerified, Primary: domainR.IsPrimary, } - _, err := s.command.AddOrgDomain(ctx, org.GetOrgId(), domainR.DomainName, []string{}) + _, err := s.command.AddOrgDomain(ctx, org.GetOrgId(), domainR.DomainName, []string{}, nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: errorToImportError(err)}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index ca5731c43..2b2137a6f 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -28,7 +28,7 @@ func (s *Server) SetDefaultOrg(ctx context.Context, req *admin_pb.SetDefaultOrgR } func (s *Server) RemoveOrg(ctx context.Context, req *admin_pb.RemoveOrgRequest) (*admin_pb.RemoveOrgResponse, error) { - details, err := s.command.RemoveOrg(ctx, req.OrgId) + details, err := s.command.RemoveOrg(ctx, req.OrgId, nil, true) if err != nil { return nil, err } @@ -87,7 +87,7 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* Roles: req.Roles, }, }, - }, true, userIDs...) + }, true, nil, userIDs...) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index 092dcdd6e..d9d841a38 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -98,7 +98,7 @@ func (s *Server) AddOrg(ctx context.Context, req *mgmt_pb.AddOrgRequest) (*mgmt_ func (s *Server) UpdateOrg(ctx context.Context, req *mgmt_pb.UpdateOrgRequest) (*mgmt_pb.UpdateOrgResponse, error) { ctxData := authz.GetCtxData(ctx) - org, err := s.command.ChangeOrg(ctx, ctxData.OrgID, req.Name) + org, err := s.command.ChangeOrg(ctx, ctxData.OrgID, req.Name, nil) if err != nil { return nil, err } @@ -112,7 +112,7 @@ func (s *Server) UpdateOrg(ctx context.Context, req *mgmt_pb.UpdateOrgRequest) ( } func (s *Server) DeactivateOrg(ctx context.Context, req *mgmt_pb.DeactivateOrgRequest) (*mgmt_pb.DeactivateOrgResponse, error) { - objectDetails, err := s.command.DeactivateOrg(ctx, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.DeactivateOrg(ctx, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -122,7 +122,7 @@ func (s *Server) DeactivateOrg(ctx context.Context, req *mgmt_pb.DeactivateOrgRe } func (s *Server) ReactivateOrg(ctx context.Context, req *mgmt_pb.ReactivateOrgRequest) (*mgmt_pb.ReactivateOrgResponse, error) { - objectDetails, err := s.command.ReactivateOrg(ctx, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.ReactivateOrg(ctx, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -132,7 +132,7 @@ func (s *Server) ReactivateOrg(ctx context.Context, req *mgmt_pb.ReactivateOrgRe } func (s *Server) RemoveOrg(ctx context.Context, req *mgmt_pb.RemoveOrgRequest) (*mgmt_pb.RemoveOrgResponse, error) { - details, err := s.command.RemoveOrg(ctx, authz.GetCtxData(ctx).OrgID) + details, err := s.command.RemoveOrg(ctx, authz.GetCtxData(ctx).OrgID, nil, true) if err != nil { return nil, err } @@ -171,7 +171,7 @@ func (s *Server) ListOrgDomains(ctx context.Context, req *mgmt_pb.ListOrgDomains } queries.Queries = append(queries.Queries, orgIDQuery) - domains, err := s.query.SearchOrgDomains(ctx, queries, false) + domains, err := s.query.SearchOrgDomains(ctx, queries, false, false) if err != nil { return nil, err } @@ -187,7 +187,7 @@ func (s *Server) AddOrgDomain(ctx context.Context, req *mgmt_pb.AddOrgDomainRequ if err != nil { return nil, err } - details, err := s.command.AddOrgDomain(ctx, orgID, req.Domain, userIDs) + details, err := s.command.AddOrgDomain(ctx, orgID, req.Domain, userIDs, nil) if err != nil { return nil, err } @@ -197,7 +197,7 @@ func (s *Server) AddOrgDomain(ctx context.Context, req *mgmt_pb.AddOrgDomainRequ } func (s *Server) RemoveOrgDomain(ctx context.Context, req *mgmt_pb.RemoveOrgDomainRequest) (*mgmt_pb.RemoveOrgDomainResponse, error) { - details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req)) + details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req), nil) if err != nil { return nil, err } @@ -207,7 +207,7 @@ func (s *Server) RemoveOrgDomain(ctx context.Context, req *mgmt_pb.RemoveOrgDoma } func (s *Server) GenerateOrgDomainValidation(ctx context.Context, req *mgmt_pb.GenerateOrgDomainValidationRequest) (*mgmt_pb.GenerateOrgDomainValidationResponse, error) { - token, url, err := s.command.GenerateOrgDomainValidation(ctx, GenerateOrgDomainValidationRequestToDomain(ctx, req)) + token, url, err := s.command.GenerateOrgDomainValidation(ctx, GenerateOrgDomainValidationRequestToDomain(ctx, req), nil) if err != nil { return nil, err } @@ -232,7 +232,7 @@ func (s *Server) ValidateOrgDomain(ctx context.Context, req *mgmt_pb.ValidateOrg if err != nil { return nil, err } - details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, req), userIDs) + details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, req), userIDs, nil) if err != nil { return nil, err } @@ -324,7 +324,7 @@ func (s *Server) ListOrgMetadata(ctx context.Context, req *mgmt_pb.ListOrgMetada if err != nil { return nil, err } - res, err := s.query.SearchOrgMetadata(ctx, true, authz.GetCtxData(ctx).OrgID, metadataQueries, false) + res, err := s.query.SearchOrgMetadata(ctx, true, authz.GetCtxData(ctx).OrgID, metadataQueries, false, false) if err != nil { return nil, err } @@ -359,7 +359,7 @@ func (s *Server) SetOrgMetadata(ctx context.Context, req *mgmt_pb.SetOrgMetadata } func (s *Server) BulkSetOrgMetadata(ctx context.Context, req *mgmt_pb.BulkSetOrgMetadataRequest) (*mgmt_pb.BulkSetOrgMetadataResponse, error) { - result, err := s.command.BulkSetOrgMetadata(ctx, authz.GetCtxData(ctx).OrgID, BulkSetOrgMetadataToDomain(req)...) + result, err := s.command.BulkSetOrgMetadata(ctx, authz.GetCtxData(ctx).OrgID, nil, BulkSetOrgMetadataToDomain(req)...) if err != nil { return nil, err } @@ -379,7 +379,7 @@ func (s *Server) RemoveOrgMetadata(ctx context.Context, req *mgmt_pb.RemoveOrgMe } func (s *Server) BulkRemoveOrgMetadata(ctx context.Context, req *mgmt_pb.BulkRemoveOrgMetadataRequest) (*mgmt_pb.BulkRemoveOrgMetadataResponse, error) { - result, err := s.command.BulkRemoveOrgMetadata(ctx, authz.GetCtxData(ctx).OrgID, req.Keys...) + result, err := s.command.BulkRemoveOrgMetadata(ctx, authz.GetCtxData(ctx).OrgID, nil, req.Keys...) if err != nil { return nil, err } diff --git a/internal/api/grpc/metadata/v2/metadata.go b/internal/api/grpc/metadata/v2/metadata.go index f50ad57f6..be10beec2 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 1f4b6585c..96ad7d5fb 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 e390aa3b8..e2dad4a14 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -4,48 +4,21 @@ 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(), - } -} - // TODO(IAM-Marco): When permission checks will be implemented, this test needs to be updated to // add the feature flag switch: // @@ -54,428 +27,336 @@ func createOrganizationWithCustomOrgID(ctx context.Context, name string, orgID s // See TestServer_ListOrganizations in org/v2beta/integration_test // See https://github.com/zitadel/zitadel/issues/10219 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) + + // deactivate 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(), }, }, }, @@ -483,91 +364,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 42832d147..d10315974 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" ) @@ -17,13 +20,123 @@ func (s *Server) AddOrganization(ctx context.Context, request *connect.Request[o if err != nil { return nil, err } - createdOrg, err := s.command.SetUpOrg(ctx, orgSetup, false) + createdOrg, err := s.command.SetUpOrg(ctx, orgSetup, false, s.command.CheckPermissionOrganizationCreate) if err != nil { return nil, err } 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 1fff47f49..a42d2ab22 100644 --- a/internal/api/grpc/org/v2/query.go +++ b/internal/api/grpc/org/v2/query.go @@ -7,10 +7,14 @@ import ( orgv2 "github.com/zitadel/zitadel/backend/v3/api/org/v2" "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" ) @@ -33,6 +37,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) @@ -112,6 +191,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: @@ -141,3 +222,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 6fd318d11..cfd8a03ac 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/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index 165f32a76..b61ba43af 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -19,17 +19,21 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta" v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) var ( - CTX context.Context - Instance *integration.Instance - Client v2beta_org.OrganizationServiceClient - User *user.AddHumanUserResponse + CTX context.Context + Instance *integration.Instance + Client v2beta_org.OrganizationServiceClient + User *user.AddHumanUserResponse + OtherOrganization *org.AddOrganizationResponse ) func TestMain(m *testing.M) { @@ -42,6 +46,7 @@ func TestMain(m *testing.M) { CTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeIAMOwner) User = Instance.CreateHumanUser(CTX) + OtherOrganization = Instance.CreateOrganization(CTX, integration.OrganizationName(), integration.Email()) return m.Run() }()) } @@ -314,12 +319,14 @@ func TestServer_UpdateOrganization(t *testing.T) { }) tests := []struct { name string + ctx context.Context req *v2beta_org.UpdateOrganizationRequest want *v2beta_org.UpdateOrganizationResponse wantErr bool }{ { name: "update org with new name", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), req: &v2beta_org.UpdateOrganizationRequest{ Id: orgs[0+(i*cases)].GetId(), Name: fmt.Sprintf("new org name %d", i), @@ -327,6 +334,7 @@ func TestServer_UpdateOrganization(t *testing.T) { }, { name: "update org with same name", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), req: &v2beta_org.UpdateOrganizationRequest{ Id: orgs[1+(i*cases)].GetId(), Name: orgNames[1+(i*cases)], @@ -335,6 +343,7 @@ func TestServer_UpdateOrganization(t *testing.T) { }, { name: "update org with non existent org id", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), req: &v2beta_org.UpdateOrganizationRequest{ Id: "non existent org id", Name: "new name", @@ -343,16 +352,26 @@ func TestServer_UpdateOrganization(t *testing.T) { }, { name: "update org with no id", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), req: &v2beta_org.UpdateOrganizationRequest{ Id: " ", Name: "new name", }, wantErr: true, }, + { + name: "no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgs[1+(i*cases)].GetId(), + Name: integration.OrganizationName(), + }, + wantErr: !stateCase.FeatureSet.GetEnableRelationalTables(), //TODO: implement for relational tables + }, } for _, tt := range tests { t.Run(fmt.Sprintf("%s - %s", stateCase.State, tt.name), func(t1 *testing.T) { - got, err := Client.UpdateOrganization(ctx, tt.req) + got, err := Client.UpdateOrganization(tt.ctx, tt.req) if tt.wantErr { require.Error(t1, err) return @@ -394,33 +413,43 @@ func TestServer_ListOrganizations(t *testing.T) { name string ctx context.Context query []*v2beta_org.OrganizationSearchFilter - want []*v2beta_org.Organization + want *v2beta_org.ListOrganizationsResponse err error }{ - { - name: "list organizations, without required permissions", - ctx: listOrgInstance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), - err: errors.New("membership not found"), - }, + // TODO: re-enable when permission model is implemented in relational tables + //{ + // name: "list organizations, without required permissions", + // ctx: listOrgInstance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + // want: &v2beta_org.ListOrganizationsResponse{ + // Pagination: &filter.PaginationResponse{ + // TotalResult: 4, + // }, + // }, + //}, { name: "list organizations happy path, no filter", ctx: listOrgIAmOwnerCtx, - want: []*v2beta_org.Organization{ - { - // default org - Name: "testinstance", + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, }, - { - Id: orgs[0].Id, - Name: orgsName[0], - }, - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - { - Id: orgs[2].Id, - Name: orgsName[2], + Organizations: []*v2beta_org.Organization{ + { + Id: listOrgInstance.DefaultOrg.Id, + Name: listOrgInstance.DefaultOrg.Name, + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, }, }, }, @@ -436,10 +465,15 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, }, }, }, @@ -455,18 +489,23 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - // default org - Name: "testinstance", + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, }, - { - Id: orgs[0].Id, - Name: orgsName[0], - }, - { - Id: orgs[2].Id, - Name: orgsName[2], + Organizations: []*v2beta_org.Organization{ + { + Id: listOrgInstance.DefaultOrg.Id, + Name: listOrgInstance.DefaultOrg.Name, + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, }, }, }, @@ -482,10 +521,15 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, }, }, }, @@ -501,6 +545,12 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + }, + Organizations: nil, + }, }, { name: "list organizations specify org name equals", @@ -515,10 +565,15 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, }, }, }, @@ -537,10 +592,15 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, }, }, }, @@ -559,10 +619,15 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, }, }, }, @@ -579,10 +644,15 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, }, }, }, @@ -599,10 +669,15 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, }, }, }, @@ -619,10 +694,15 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, - want: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, }, }, }, @@ -639,6 +719,10 @@ func TestServer_ListOrganizations(t *testing.T) { require.EventuallyWithT(t, func(ttt *assert.CollectT) { got, err := listOrgClient.ListOrganizations(tt.ctx, &v2beta_org.ListOrganizationsRequest{ Filter: tt.query, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: v2beta_org.OrgFieldName_ORG_FIELD_NAME_CREATION_DATE, }) if tt.err != nil { require.ErrorContains(ttt, err, tt.err.Error()) @@ -646,32 +730,21 @@ func TestServer_ListOrganizations(t *testing.T) { } require.NoError(ttt, err) - require.Equal(ttt, uint64(len(tt.want)), got.Pagination.GetTotalResult()) + require.Equal(ttt, tt.want.GetPagination(), got.GetPagination()) - foundOrgs := 0 - for _, got := range got.Organizations { - for _, org := range tt.want { + require.Len(ttt, got.Organizations, len(tt.want.Organizations)) - // created/chagned date - gotCD := got.GetCreationDate().AsTime() - now := time.Now() - assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) - gotCD = got.GetChangedDate().AsTime() - assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + for i, got := range got.Organizations { + // created/chagned date + gotCD := got.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + gotCD = got.GetChangedDate().AsTime() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) - // default org - if org.Name == got.Name && got.Name == "testinstance" { - foundOrgs += 1 - continue - } - - if org.Name == got.Name && - org.Id == got.Id { - foundOrgs += 1 - } - } + assert.Equal(ttt, tt.want.Organizations[i].Id, got.Id) + assert.Equal(ttt, tt.want.Organizations[i].Name, got.Name) } - require.Equal(ttt, len(tt.want), foundOrgs) }, retryDuration, tick, "timeout waiting for expected organizations being created") }) } @@ -710,7 +783,7 @@ func TestServer_DeleteOrganization(t *testing.T) { }{ { name: "delete org no permission", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), req: &v2beta_org.DeleteOrganizationRequest{ Id: orgs[0+(orgsNumPerCase*i)].GetId(), }, @@ -822,10 +895,15 @@ func TestServer_ActivateOrganization(t *testing.T) { }, { name: "Activate, no permission", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), inputOrgID: orgs[0+(orgsNumPerCase*i)].GetId(), - // BUG: this needs changing - err: errors.New("membership not found"), + err: func() error { + //TODO: implement permissions for relational tables + if stateCase.FeatureSet.GetEnableRelationalTables() { + return errors.New("Organisation is already active") + } + return errors.New("membership not found") + }(), }, { name: "Activate, not existing", @@ -904,11 +982,15 @@ func TestServer_DeactivateOrganization(t *testing.T) { }, { name: "Deactivate, no permission", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), inputOrgID: orgs[0+(orgsNumPerCase*i)].GetId(), - - // BUG: this needs changing - err: errors.New("membership not found"), + err: func() error { + //TODO: implement for relational tables + if stateCase.FeatureSet.GetEnableRelationalTables() { + return errors.New("Organisation is already deactivated") + } + return errors.New("membership not found") + }(), }, { name: "Deactivate, not existing", @@ -952,6 +1034,7 @@ func TestServer_AddOrganizationDomain(t *testing.T) { }{ { name: "add org domain, happy path", + ctx: CTX, domain: integration.DomainName(), testFunc: func() string { orgs, _, _ := createOrgs(CTX, t, Client, 1) @@ -960,9 +1043,22 @@ func TestServer_AddOrganizationDomain(t *testing.T) { }, }, { - name: "add org domain, twice", + name: "no permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), domain: integration.DomainName(), testFunc: func() string { + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].Id + 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].Id @@ -1000,33 +1096,32 @@ func TestServer_AddOrganizationDomain(t *testing.T) { }, { name: "add org domain to non existent org", + ctx: CTX, domain: integration.DomainName(), testFunc: func() string { return "non-existing-org-id" }, - // BUG: should return a error - err: nil, + err: errors.New("Organisation not found"), }, } for _, tt := range tests { - var orgId string t.Run(tt.name, func(t *testing.T) { - orgId = tt.testFunc() + orgId := tt.testFunc() + addOrgDomainRes, err := Client.AddOrganizationDomain(tt.ctx, &v2beta_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)) + } }) - addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_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)) - } } } @@ -1098,60 +1193,155 @@ func TestServer_AddOrganizationDomain_ClaimDomain(t *testing.T) { func TestServer_ListOrganizationDomains(t *testing.T) { domain := integration.DomainName() + + orgs, _, _ := createOrgs(CTX, t, Client, 1) + orgId := orgs[0].Id + + var primaryDomain string + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Second) + require.EventuallyWithT(t, func(t *assert.CollectT) { + organizations, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + {Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{Id: orgId}, + }}, + }, + }) + require.NoError(t, err) + require.Len(t, organizations.GetOrganizations(), 1) + primaryDomain = organizations.GetOrganizations()[0].GetPrimaryDomain() + }, retryDuration, tick, "could not find primary domain") + + _, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + request *v2beta_org.ListOrganizationDomainsRequest + } + type want struct { + response *v2beta_org.ListOrganizationDomainsResponse + err bool + } + tests := []struct { - name string - ctx context.Context - domain string - testFunc func() string - err error + name string + args args + want want }{ { - name: "list org domain, happy path", - domain: domain, - testFunc: func() string { - // 1. create organization - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id - // 2. add domain - addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + name: "non existing organization", + args: args{ + ctx: CTX, + request: &v2beta_org.ListOrganizationDomainsRequest{OrganizationId: "not-existing"}, + }, + want: want{ + response: &v2beta_org.ListOrganizationDomainsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + }, + Domains: nil, + }, + }, + }, + { + name: "no permission (different organization), error", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + request: &v2beta_org.ListOrganizationDomainsRequest{ 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)) - - return orgId + }, + }, + want: want{ + response: &v2beta_org.ListOrganizationDomainsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + }, + Domains: nil, + }, + }, + }, + { + name: "list org domain, all domains", + args: args{ + ctx: CTX, + request: &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }, + }, + want: want{ + response: &v2beta_org.ListOrganizationDomainsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + }, + Domains: []*v2beta_org.Domain{ + { + OrganizationId: orgId, + DomainName: domain, + IsVerified: true, + IsPrimary: false, + ValidationType: 0, + }, + { + OrganizationId: orgId, + DomainName: primaryDomain, + IsVerified: true, + IsPrimary: true, + ValidationType: 0, + }, + }, + }, + }, + }, + { + name: "list specific domain", + args: args{ + ctx: CTX, + request: &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + Filters: []*v2beta_org.DomainSearchFilter{ + {Filter: &v2beta_org.DomainSearchFilter_DomainNameFilter{DomainNameFilter: &v2beta_org.DomainNameFilter{Name: domain}}}, + }, + }, + }, + want: want{ + response: &v2beta_org.ListOrganizationDomainsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Domains: []*v2beta_org.Domain{ + { + OrganizationId: orgId, + DomainName: domain, + IsVerified: true, + IsPrimary: false, + ValidationType: 0, + }, + }, + }, }, }, } for _, tt := range tests { - var orgId string t.Run(tt.name, func(t *testing.T) { - orgId = tt.testFunc() - }) - - var err error - var queryRes *v2beta_org.ListOrganizationDomainsResponse - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - queryRes, err = Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ - OrganizationId: orgId, - }) - require.NoError(ttt, err) - found := false - for _, res := range queryRes.Domains { - if res.DomainName == tt.domain { - found = true + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, 10*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.True(ttt, found, "unable to find added domain") - }, retryDuration, tick, "timeout waiting for adding domain") + require.NoError(ttt, err) + 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") + }) } } @@ -1166,6 +1356,7 @@ func TestServer_DeleteOrganizationDomain(t *testing.T) { }{ { name: "delete org domain, happy path", + ctx: CTX, domain: domain, testFunc: func() string { // 1. create organization @@ -1200,6 +1391,7 @@ func TestServer_DeleteOrganizationDomain(t *testing.T) { }, { name: "delete org domain, twice", + ctx: CTX, domain: integration.DomainName(), testFunc: func() string { // 1. create organization @@ -1246,6 +1438,7 @@ func TestServer_DeleteOrganizationDomain(t *testing.T) { }, { name: "delete org domain to non existent org", + ctx: CTX, domain: integration.DomainName(), testFunc: func() string { return "non-existing-org-id" @@ -1253,13 +1446,49 @@ func TestServer_DeleteOrganizationDomain(t *testing.T) { // BUG: 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].Id + + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_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, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(ttt, err) + + found := slices.ContainsFunc(queryRes.Domains, func(d *v2beta_org.Domain) bool { return d.GetDomainName() == 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(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + _, err := Client.DeleteOrganizationDomain(tt.ctx, &v2beta_org.DeleteOrganizationDomainRequest{ OrganizationId: orgId, Domain: tt.domain, }) @@ -1474,7 +1703,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { err: errors.New("Domain doesn't exist on organization"), }, { - name: "validate org non existnetn domain", + name: "validate org non existent domain", ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: orgId, @@ -1483,6 +1712,16 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { }, err: errors.New("Domain doesn't exist on organization"), }, + { + name: "validate without permission", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("membership not found"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1512,6 +1751,14 @@ func TestServer_SetOrganizationMetadata(t *testing.T) { 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), @@ -1619,109 +1866,168 @@ func TestServer_SetOrganizationMetadata(t *testing.T) { func TestServer_ListOrganizationMetadata(t *testing.T) { orgs, _, _ := createOrgs(CTX, t, Client, 1) orgId := orgs[0].Id + setRespoonse, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_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) + + type args struct { + ctx context.Context + request *v2beta_org.ListOrganizationMetadataRequest + } + type want struct { + response *v2beta_org.ListOrganizationMetadataResponse + err error + } tests := []struct { - name string - ctx context.Context - setupFunc func() - orgId string - keyValuePairs []struct { - key string - value string - } + name string + args args + want want }{ { name: "list org metadata happy path", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - setupFunc: func() { - _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + request: &v2beta_org.ListOrganizationMetadataRequest{ OrganizationId: orgId, - Metadata: []*v2beta_org.Metadata{ + }, + }, + want: want{ + response: &v2beta_org.ListOrganizationMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + }, + Metadata: []*metadata.Metadata{ { - Key: "key1", - Value: []byte("value1"), + 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(), }, }, - }) - require.NoError(t, err) - }, - orgId: orgId, - keyValuePairs: []struct{ key, value string }{ - { - key: "key1", - value: "value1", }, }, }, { - name: "list multiple org metadata happy path", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - setupFunc: func() { - _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + name: "list org metadata filter key", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + request: &v2beta_org.ListOrganizationMetadataRequest{ OrganizationId: orgId, - Metadata: []*v2beta_org.Metadata{ + Pagination: &filter.PaginationRequest{ + Offset: 1, + Limit: 2, + }, + Filter: []*metadata.MetadataQuery{ { - Key: "key2", - Value: []byte("value2"), - }, - { - Key: "key3", - Value: []byte("value3"), - }, - { - Key: "key4", - Value: []byte("value4"), + Query: &metadata.MetadataQuery_KeyQuery{ + KeyQuery: &metadata.MetadataKeyQuery{ + Key: "key2", + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH, + }, + }, }, }, - }) - require.NoError(t, err) + }, }, - orgId: orgId, - keyValuePairs: []struct{ key, value string }{ - { - key: "key2", - value: "value2", - }, - { - key: "key3", - value: "value3", - }, - { - key: "key4", - value: "value4", + want: want{ + response: &v2beta_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", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - orgId: "non existent orgid", - keyValuePairs: []struct{ key, value string }{}, + name: "list org metadata for non existent org", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), + request: &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: "non existent orgid", + }, + }, + want: want{ + response: &v2beta_org.ListOrganizationMetadataResponse{ + Pagination: &filter.PaginationResponse{}, + }, + }, + }, + { + name: "list org metadata without permission (other organization)", + args: args{ + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + request: &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }, + }, + want: want{ + response: &v2beta_org.ListOrganizationMetadataResponse{ + Pagination: &filter.PaginationResponse{}, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.setupFunc != nil { - tt.setupFunc() - } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 1*time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ - OrganizationId: tt.orgId, - }) + got, err := Client.ListOrganizationMetadata(tt.args.ctx, tt.args.request) require.NoError(ttt, err) - foundMetadataCount := 0 - for _, kv := range tt.keyValuePairs { - for _, res := range got.Metadata { - if res.Key == kv.key && - string(res.Value) == kv.value { - foundMetadataCount += 1 - } - } - } - require.Len(ttt, tt.keyValuePairs, foundMetadataCount) + assert.EqualExportedValues(ttt, tt.want.response, got) }, retryDuration, tick, "timeout waiting for expected organizations being created") }) } @@ -1731,6 +2037,39 @@ func TestServer_DeleteOrganizationMetadata(t *testing.T) { orgs, _, _ := createOrgs(CTX, t, Client, 1) orgId := orgs[0].Id + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_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, &v2beta_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 @@ -1740,27 +2079,11 @@ func TestServer_DeleteOrganizationMetadata(t *testing.T) { key string value string } - metadataToRemain []struct { - key string - value string - } err error }{ { - name: "delete org metadata happy path", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - setupFunc: func() { - _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ - OrganizationId: orgId, - Metadata: []*v2beta_org.Metadata{ - { - Key: "key1", - Value: []byte("value1"), - }, - }, - }) - require.NoError(t, err) - }, + name: "delete org metadata happy path", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), orgId: orgId, metadataToDelete: []struct{ key, value string }{ { @@ -1770,24 +2093,8 @@ func TestServer_DeleteOrganizationMetadata(t *testing.T) { }, }, { - name: "delete multiple org metadata happy path", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - setupFunc: func() { - _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ - OrganizationId: orgId, - Metadata: []*v2beta_org.Metadata{ - { - Key: "key2", - Value: []byte("value2"), - }, - { - Key: "key3", - Value: []byte("value3"), - }, - }, - }) - require.NoError(t, err) - }, + name: "delete multiple org metadata happy path", + ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), orgId: orgId, metadataToDelete: []struct{ key, value string }{ { @@ -1801,118 +2108,44 @@ func TestServer_DeleteOrganizationMetadata(t *testing.T) { }, }, { - name: "delete some org metadata but not all", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - setupFunc: func() { - _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ - OrganizationId: orgId, - Metadata: []*v2beta_org.Metadata{ - { - Key: "key4", - Value: []byte("value4"), - }, - // key5 should not be deleted - { - Key: "key5", - Value: []byte("value5"), - }, - { - Key: "key6", - Value: []byte("value6"), - }, - }, - }) - require.NoError(t, err) + 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", }, - { - key: "key6", - value: "value6", - }, }, - metadataToRemain: []struct{ key, value string }{ - { - key: "key5", - value: "value5", - }, - }, - }, - { - name: "delete org metadata that does not exist", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - setupFunc: func() { - _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ - OrganizationId: orgId, - Metadata: []*v2beta_org.Metadata{ - { - Key: "key88", - Value: []byte("value74"), - }, - { - Key: "key5888", - Value: []byte("value8885"), - }, - }, - }) - require.NoError(t, err) - }, - orgId: orgId, - // TODO: this error message needs to be either removed or changed - err: errors.New("Metadata list is empty"), - }, - { - name: "delete org metadata for org that does not exist", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - setupFunc: func() { - _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ - OrganizationId: orgId, - Metadata: []*v2beta_org.Metadata{ - { - Key: "key88", - Value: []byte("value74"), - }, - { - Key: "key5888", - Value: []byte("value8885"), - }, - }, - }) - require.NoError(t, err) - }, - orgId: "non existant org id", - // TODO: this error message needs to be either removed or changed - err: errors.New("Metadata list is empty"), + err: errors.New("membership not found"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.setupFunc != nil { - tt.setupFunc() - } - - // check metadata exists - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_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, len(tt.metadataToDelete), foundMetadataCount) - }, retryDuration, tick, "timeout waiting for expected organizations being created") keys := make([]string, len(tt.metadataToDelete)) for i, kvp := range tt.metadataToDelete { @@ -1930,7 +2163,7 @@ func TestServer_DeleteOrganizationMetadata(t *testing.T) { } require.NoError(t, err) - retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + 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, &v2beta_org.ListOrganizationMetadataRequest{ @@ -1948,22 +2181,6 @@ func TestServer_DeleteOrganizationMetadata(t *testing.T) { } require.Equal(ttt, foundMetadataCount, 0) }, retryDuration, tick, "timeout waiting for expected organizations being created") - - // check metadata that should not be delted was not deleted - listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ - OrganizationId: tt.orgId, - }) - require.NoError(t, err) - foundMetadataCount := 0 - for _, kv := range tt.metadataToRemain { - for _, res := range listOrgMetadataRes.Metadata { - if res.Key == kv.key && - string(res.Value) == kv.value { - foundMetadataCount += 1 - } - } - } - require.Equal(t, len(tt.metadataToRemain), foundMetadataCount) }) } } diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index 9c1122d4e..7f3a78c1b 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -2,7 +2,6 @@ package org import ( "context" - "errors" "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" @@ -25,7 +24,7 @@ func (s *Server) CreateOrganization(ctx context.Context, request *connect.Reques if err != nil { return nil, err } - createdOrg, err := s.command.SetUpOrg(ctx, orgSetup, false) + createdOrg, err := s.command.SetUpOrg(ctx, orgSetup, false, s.command.CheckPermissionOrganizationCreate) if err != nil { return nil, err } @@ -37,7 +36,7 @@ func (s *Server) UpdateOrganization(ctx context.Context, request *connect.Reques return orgv2.UpdateOrganizationBeta(ctx, request) } - org, err := s.command.ChangeOrg(ctx, request.Msg.GetId(), request.Msg.GetName()) + org, err := s.command.ChangeOrg(ctx, request.Msg.GetId(), request.Msg.GetName(), s.command.CheckPermissionOrganizationWrite) if err != nil { return nil, err } @@ -74,12 +73,8 @@ func (s *Server) DeleteOrganization(ctx context.Context, request *connect.Reques return orgv2.DeleteOrganizationBeta(ctx, request) } - details, err := s.command.RemoveOrg(ctx, request.Msg.GetId()) + details, err := s.command.RemoveOrg(ctx, request.Msg.GetId(), s.command.CheckPermissionOrganizationDelete, false) if err != nil { - var notFoundError *zerrors.NotFoundError - if errors.As(err, ¬FoundError) { - return connect.NewResponse(&v2beta_org.DeleteOrganizationResponse{}), nil - } return nil, err } return connect.NewResponse(&v2beta_org.DeleteOrganizationResponse{ @@ -88,7 +83,7 @@ func (s *Server) DeleteOrganization(ctx context.Context, request *connect.Reques } func (s *Server) SetOrganizationMetadata(ctx context.Context, request *connect.Request[v2beta_org.SetOrganizationMetadataRequest]) (*connect.Response[v2beta_org.SetOrganizationMetadataResponse], error) { - result, err := s.command.BulkSetOrgMetadata(ctx, request.Msg.GetOrganizationId(), BulkSetOrgMetadataToDomain(request.Msg)...) + result, err := s.command.BulkSetOrgMetadata(ctx, request.Msg.GetOrganizationId(), s.command.CheckPermissionOrganizationWrite, BulkSetOrgMetadataToDomain(request.Msg)...) if err != nil { return nil, err } @@ -102,7 +97,7 @@ func (s *Server) ListOrganizationMetadata(ctx context.Context, request *connect. if err != nil { return nil, err } - res, err := s.query.SearchOrgMetadata(ctx, true, request.Msg.GetOrganizationId(), metadataQueries, false) + res, err := s.query.SearchOrgMetadata(ctx, true, request.Msg.GetOrganizationId(), metadataQueries, false, true) if err != nil { return nil, err } @@ -116,7 +111,7 @@ func (s *Server) ListOrganizationMetadata(ctx context.Context, request *connect. } func (s *Server) DeleteOrganizationMetadata(ctx context.Context, request *connect.Request[v2beta_org.DeleteOrganizationMetadataRequest]) (*connect.Response[v2beta_org.DeleteOrganizationMetadataResponse], error) { - result, err := s.command.BulkRemoveOrgMetadata(ctx, request.Msg.GetOrganizationId(), request.Msg.Keys...) + result, err := s.command.BulkRemoveOrgMetadata(ctx, request.Msg.GetOrganizationId(), s.command.CheckPermissionOrganizationWrite, request.Msg.Keys...) if err != nil { return nil, err } @@ -130,7 +125,7 @@ func (s *Server) DeactivateOrganization(ctx context.Context, request *connect.Re return orgv2.DeactivateOrganizationBeta(ctx, request) } - objectDetails, err := s.command.DeactivateOrg(ctx, request.Msg.GetId()) + objectDetails, err := s.command.DeactivateOrg(ctx, request.Msg.GetId(), s.command.CheckPermissionOrganizationWrite) if err != nil { return nil, err } @@ -144,7 +139,7 @@ func (s *Server) ActivateOrganization(ctx context.Context, request *connect.Requ return orgv2.ActivateOrganizationBeta(ctx, request) } - objectDetails, err := s.command.ReactivateOrg(ctx, request.Msg.GetId()) + objectDetails, err := s.command.ReactivateOrg(ctx, request.Msg.GetId(), s.command.CheckPermissionOrganizationWrite) if err != nil { return nil, err } @@ -158,7 +153,7 @@ func (s *Server) AddOrganizationDomain(ctx context.Context, request *connect.Req if err != nil { return nil, err } - details, err := s.command.AddOrgDomain(ctx, request.Msg.GetOrganizationId(), request.Msg.GetDomain(), userIDs) + details, err := s.command.AddOrgDomain(ctx, request.Msg.GetOrganizationId(), request.Msg.GetDomain(), userIDs, s.command.CheckPermissionOrganizationWrite) if err != nil { return nil, err } @@ -178,7 +173,7 @@ func (s *Server) ListOrganizationDomains(ctx context.Context, req *connect.Reque } queries.Queries = append(queries.Queries, orgIDQuery) - domains, err := s.query.SearchOrgDomains(ctx, queries, false) + domains, err := s.query.SearchOrgDomains(ctx, queries, false, true) if err != nil { return nil, err } @@ -192,7 +187,7 @@ func (s *Server) ListOrganizationDomains(ctx context.Context, req *connect.Reque } func (s *Server) DeleteOrganizationDomain(ctx context.Context, req *connect.Request[org.DeleteOrganizationDomainRequest]) (*connect.Response[org.DeleteOrganizationDomainResponse], error) { - details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req.Msg)) + details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req.Msg), s.command.CheckPermissionOrganizationWrite) if err != nil { return nil, err } @@ -202,7 +197,7 @@ func (s *Server) DeleteOrganizationDomain(ctx context.Context, req *connect.Requ } 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(ctx, req.Msg)) + token, url, err := s.command.GenerateOrgDomainValidation(ctx, GenerateOrgDomainValidationRequestToDomain(ctx, req.Msg), s.command.CheckPermissionOrganizationWrite) if err != nil { return nil, err } @@ -217,7 +212,7 @@ func (s *Server) VerifyOrganizationDomain(ctx context.Context, request *connect. if err != nil { return nil, err } - details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, request.Msg), userIDs) + details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, request.Msg), userIDs, s.command.CheckPermissionOrganizationWrite) if err != nil { return nil, err } diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index d3e2dbb2c..fe0c440b5 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -131,6 +131,7 @@ func (l *Login) runPostExternalAuthenticationActions( resourceOwner, &query.OrgMetadataSearchQueries{}, false, + false, ) if err != nil { logging.WithError(err).Info("unable to get org metadata in action") @@ -326,6 +327,7 @@ func (l *Login) runPreCreationActions( resourceOwner, &query.OrgMetadataSearchQueries{}, false, + false, ) if err != nil { logging.WithError(err).Info("unable to get org metadata in action") @@ -402,6 +404,7 @@ func (l *Login) runPostCreationActions( resourceOwner, &query.OrgMetadataSearchQueries{}, false, + false, ) if err != nil { logging.WithError(err).Info("unable to get org metadata in action") diff --git a/internal/api/ui/login/register_option_handler.go b/internal/api/ui/login/register_option_handler.go index 31270c044..ad45d4ec0 100644 --- a/internal/api/ui/login/register_option_handler.go +++ b/internal/api/ui/login/register_option_handler.go @@ -105,7 +105,7 @@ func (l *Login) passLoginHintToRegistration(r *http.Request, authReq *domain.Aut logging.WithFields("authRequest", authReq.ID, "org", authReq.RequestedOrgID).Error("unable to search query for registration loginHint") return data } - domains, err := l.query.SearchOrgDomains(r.Context(), &query.OrgDomainSearchQueries{Queries: []query.SearchQuery{searchQuery}}, false) + domains, err := l.query.SearchOrgDomains(r.Context(), &query.OrgDomainSearchQueries{Queries: []query.SearchQuery{searchQuery}}, false, false) if err != nil { logging.WithFields("authRequest", authReq.ID, "org", authReq.RequestedOrgID).Error("unable to load domains for registration loginHint") return data diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index 58a49f1d0..33c56632b 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -84,7 +84,7 @@ func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) { l.renderRegisterOrg(w, r, authRequest, data, err) return } - _, err = l.command.SetUpOrg(ctx, data.toCommandOrg(), true, userIDs...) + _, err = l.command.SetUpOrg(ctx, data.toCommandOrg(), true, nil, userIDs...) if err != nil { l.renderRegisterOrg(w, r, authRequest, data, err) return diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 6effedefa..8b519c66d 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -255,6 +255,17 @@ func newMockProjectPermissionCheckSAMLNotAllowed() domain.ProjectPermissionCheck } } +func newMockOrganizationPermissionCheckNotAllowed() OrganizationPermissionCheck { + return func(ctx context.Context, organizationID string) (err error) { + return zerrors.ThrowPermissionDenied(nil, "", "Errors.PermissionDenied") + } +} +func newMockOrganizationPermissionCheckAllowed() OrganizationPermissionCheck { + return func(ctx context.Context, organizationID string) (err error) { + return nil + } +} + func newMockTokenVerifierValid() func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { return nil diff --git a/internal/command/org.go b/internal/command/org.go index 081de7e7e..d8e567758 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -181,7 +181,7 @@ func (c *orgSetupCommands) setupOrgAdminMachine(orgAgg *org.Aggregate, machine * func (c *orgSetupCommands) addCustomDomain(domain string, userIDs []string) error { if domain != "" { - c.validations = append(c.validations, c.commands.prepareAddOrgDomain(c.aggregate, domain, userIDs)) + c.validations = append(c.validations, c.commands.prepareAddOrgDomain(c.aggregate, domain, userIDs, nil)) } return nil } @@ -263,7 +263,7 @@ func (c *orgSetupCommands) createdMachineAdmin(admin *OrgSetupAdmin) *CreatedOrg return createdAdmin } -func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail bool, userIDs ...string) (*CreatedOrg, error) { +func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail bool, permissionCheck OrganizationPermissionCheck, userIDs ...string) (*CreatedOrg, error) { if err := o.Validate(); err != nil { return nil, err } @@ -276,6 +276,12 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail b } } + if permissionCheck != nil { + if err := permissionCheck(ctx, o.OrgID); err != nil { + return nil, err + } + } + // because users can choose their own ID, we must check that an org with the same ID does not already exist existingOrg, err := c.getOrgWriteModelByID(ctx, o.OrgID) if err != nil { @@ -394,14 +400,18 @@ func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, reso return orgWriteModelToOrg(addedOrg), nil } -func (c *Commands) ChangeOrg(ctx context.Context, orgID, name string) (*domain.ObjectDetails, error) { +func (c *Commands) ChangeOrg(ctx context.Context, organizationID, name string, permissionCheck OrganizationPermissionCheck) (*domain.ObjectDetails, error) { name = strings.TrimSpace(name) - orgID = strings.TrimSpace(orgID) - if orgID == "" || name == "" { + organizationID = strings.TrimSpace(organizationID) + if organizationID == "" || name == "" { return nil, zerrors.ThrowInvalidArgument(nil, "EVENT-Mf9sd", "Errors.Org.Invalid") } - - orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID) + if permissionCheck != nil { + if err := permissionCheck(ctx, organizationID); err != nil { + return nil, err + } + } + orgWriteModel, err := c.getOrgWriteModelByID(ctx, organizationID) if err != nil { return nil, err } @@ -414,7 +424,7 @@ func (c *Commands) ChangeOrg(ctx context.Context, orgID, name string) (*domain.O orgAgg := OrgAggregateFromWriteModel(&orgWriteModel.WriteModel) events := make([]eventstore.Command, 0) events = append(events, org.NewOrgChangedEvent(ctx, orgAgg, orgWriteModel.Name, name)) - changeDomainEvents, err := c.changeDefaultDomain(ctx, orgID, name) + changeDomainEvents, err := c.changeDefaultDomain(ctx, organizationID, name) if err != nil { return nil, err } @@ -432,8 +442,15 @@ func (c *Commands) ChangeOrg(ctx context.Context, orgID, name string) (*domain.O return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil } -func (c *Commands) DeactivateOrg(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { - orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID) +type OrganizationPermissionCheck func(ctx context.Context, organizationID string) error + +func (c *Commands) DeactivateOrg(ctx context.Context, organizationID string, permissionCheck OrganizationPermissionCheck) (*domain.ObjectDetails, error) { + if permissionCheck != nil { + if err := permissionCheck(ctx, organizationID); err != nil { + return nil, err + } + } + orgWriteModel, err := c.getOrgWriteModelByID(ctx, organizationID) if err != nil { return nil, err } @@ -455,8 +472,13 @@ func (c *Commands) DeactivateOrg(ctx context.Context, orgID string) (*domain.Obj return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil } -func (c *Commands) ReactivateOrg(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { - orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID) +func (c *Commands) ReactivateOrg(ctx context.Context, organizationID string, permissionCheck OrganizationPermissionCheck) (*domain.ObjectDetails, error) { + if permissionCheck != nil { + if err := permissionCheck(ctx, organizationID); err != nil { + return nil, err + } + } + orgWriteModel, err := c.getOrgWriteModelByID(ctx, organizationID) if err != nil { return nil, err } @@ -478,13 +500,17 @@ func (c *Commands) ReactivateOrg(ctx context.Context, orgID string) (*domain.Obj return writeModelToObjectDetails(&orgWriteModel.WriteModel), nil } -func (c *Commands) RemoveOrg(ctx context.Context, id string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveOrg(ctx context.Context, id string, permissionCheck OrganizationPermissionCheck, mustExist bool) (*domain.ObjectDetails, error) { orgAgg := org.NewAggregate(id) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRemoveOrg(orgAgg)) + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRemoveOrg(orgAgg, permissionCheck, mustExist)) if err != nil { return nil, err } + if len(cmds) == 0 { + return &domain.ObjectDetails{}, nil + } events, err := c.eventstore.Push(ctx, cmds...) if err != nil { @@ -498,9 +524,15 @@ func (c *Commands) RemoveOrg(ctx context.Context, id string) (*domain.ObjectDeta }, nil } -func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation { +//nolint:gocognit +func (c *Commands) prepareRemoveOrg(a *org.Aggregate, permissionCheck OrganizationPermissionCheck, mustExist bool) preparation.Validation { return func() (preparation.CreateCommands, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if permissionCheck != nil { + if err := permissionCheck(ctx, a.ID); err != nil { + return nil, err + } + } instance := authz.GetInstance(ctx) if a.ID == instance.DefaultOrganisationID() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-wG9p1", "Errors.Org.DefaultOrgNotDeletable") @@ -520,7 +552,10 @@ func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation { return nil, zerrors.ThrowPreconditionFailed(err, "COMMA-wG9p1", "Errors.Org.NotFound") } if !isOrgStateExists(writeModel.State) { - return nil, zerrors.ThrowNotFound(nil, "COMMA-aps2n", "Errors.Org.NotFound") + if mustExist { + return nil, zerrors.ThrowNotFound(nil, "COMMA-aps2n", "Errors.Org.NotFound") + } + return nil, nil } domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ID) diff --git a/internal/command/org_domain.go b/internal/command/org_domain.go index e13ba50f1..9e0edfce5 100644 --- a/internal/command/org_domain.go +++ b/internal/command/org_domain.go @@ -20,7 +20,8 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) prepareAddOrgDomain(a *org.Aggregate, addDomain string, userIDs []string) preparation.Validation { +//nolint:gocognit +func (c *Commands) prepareAddOrgDomain(a *org.Aggregate, addDomain string, userIDs []string, permissionCheck OrganizationPermissionCheck) preparation.Validation { return func() (preparation.CreateCommands, error) { if addDomain = strings.TrimSpace(addDomain); addDomain == "" { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument") @@ -29,6 +30,15 @@ func (c *Commands) prepareAddOrgDomain(a *org.Aggregate, addDomain string, userI ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + if permissionCheck != nil { + if err := permissionCheck(ctx, a.ID); err != nil { + return nil, err + } + } + if err := c.checkOrgExists(ctx, a.ID); err != nil { + return nil, err + } + existing, err := orgDomain(ctx, filter, a.ID, addDomain) if err != nil && !errors.Is(err, zerrors.ThrowNotFound(nil, "", "")) { return nil, err @@ -123,12 +133,13 @@ func (c *Commands) VerifyOrgDomain(ctx context.Context, orgID, domain string) (_ return pushedEventsToObjectDetails(pushedEvents), nil } -func (c *Commands) AddOrgDomain(ctx context.Context, orgID, domain string, claimedUserIDs []string) (_ *domain.ObjectDetails, err error) { +func (c *Commands) AddOrgDomain(ctx context.Context, orgID, domain string, claimedUserIDs []string, permissionCheck OrganizationPermissionCheck) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() orgAgg := org.NewAggregate(orgID) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgDomain(orgAgg, domain, claimedUserIDs)) + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgDomain(orgAgg, domain, claimedUserIDs, permissionCheck)) if err != nil { return nil, err } @@ -139,7 +150,7 @@ func (c *Commands) AddOrgDomain(ctx context.Context, orgID, domain string, claim return pushedEventsToObjectDetails(pushedEvents), nil } -func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *domain.OrgDomain) (token, url string, err error) { +func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *domain.OrgDomain, permissionCheck OrganizationPermissionCheck) (token, url string, err error) { if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { return "", "", zerrors.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") } @@ -147,6 +158,11 @@ func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *d if !ok { return "", "", zerrors.ThrowInvalidArgument(nil, "ORG-Gsw31", "Errors.Org.DomainVerificationTypeInvalid") } + if permissionCheck != nil { + if err := permissionCheck(ctx, orgDomain.AggregateID); err != nil { + return "", "", err + } + } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return "", "", err @@ -177,10 +193,15 @@ func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *d return token, url, nil } -func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain, claimedUserIDs []string) (*domain.ObjectDetails, error) { +func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain, claimedUserIDs []string, permissionCheck OrganizationPermissionCheck) (*domain.ObjectDetails, error) { if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") } + if permissionCheck != nil { + if err := permissionCheck(ctx, orgDomain.AggregateID); err != nil { + return nil, err + } + } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return nil, err @@ -261,10 +282,15 @@ func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.Or return writeModelToObjectDetails(&domainWriteModel.WriteModel), nil } -func (c *Commands) RemoveOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain, permissionCheck OrganizationPermissionCheck) (*domain.ObjectDetails, error) { if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-SJsK3", "Errors.Org.InvalidDomain") } + if permissionCheck != nil { + if err := permissionCheck(ctx, orgDomain.AggregateID); err != nil { + return nil, err + } + } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return nil, err diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go index 6aaee20c2..1c5ab30b1 100644 --- a/internal/command/org_domain_test.go +++ b/internal/command/org_domain_test.go @@ -23,11 +23,13 @@ import ( func TestAddDomain(t *testing.T) { type args struct { - a *org.Aggregate - domain string - claimedUserIDs []string - idGenerator id.Generator - filter preparation.FilterToQueryReducer + a *org.Aggregate + domain string + claimedUserIDs []string + idGenerator id.Generator + filter preparation.FilterToQueryReducer + eventstore func(*testing.T) *eventstore.Eventstore + permissionCheck OrganizationPermissionCheck } agg := org.NewAggregate("test") @@ -40,8 +42,9 @@ func TestAddDomain(t *testing.T) { { name: "invalid domain", args: args{ - a: agg, - domain: "", + a: agg, + domain: "", + eventstore: expectEventstore(), }, want: Want{ ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument"), @@ -58,6 +61,14 @@ func TestAddDomain(t *testing.T) { org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, true, true), }, nil }, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), + ), }, want: Want{ Commands: []eventstore.Command{ @@ -99,6 +110,14 @@ func TestAddDomain(t *testing.T) { return []eventstore.Event{org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, false, false)}, nil } }(), + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), + ), }, want: Want{ Commands: []eventstore.Command{ @@ -121,18 +140,67 @@ func TestAddDomain(t *testing.T) { org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"), }, nil }, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), + ), }, want: Want{ CreateErr: zerrors.ThrowAlreadyExists(nil, "", ""), }, }, + { + name: "no permission", + args: args{ + a: agg, + domain: "domain", + claimedUserIDs: []string{"userID1"}, + filter: nil, + eventstore: expectEventstore(), + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + want: Want{ + CreateErr: zerrors.ThrowPermissionDenied(nil, "", "Errors.PermissionDenied"), + }, + }, + { + name: "correct with permission check", + args: args{ + a: agg, + domain: "domain", + claimedUserIDs: []string{"userID1"}, + filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent(ctx, &agg.Aggregate, true, true, true), + }, nil + }, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), + ), + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + want: Want{ + Commands: []eventstore.Command{ + org.NewDomainAddedEvent(context.Background(), &agg.Aggregate, "domain"), + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { AssertValidation( t, http.WithRequestedHost(context.Background(), "domain"), - (&Commands{idGenerator: tt.args.idGenerator}).prepareAddOrgDomain(tt.args.a, tt.args.domain, tt.args.claimedUserIDs), + (&Commands{idGenerator: tt.args.idGenerator, eventstore: tt.args.eventstore(t)}).prepareAddOrgDomain(tt.args.a, tt.args.domain, tt.args.claimedUserIDs, tt.args.permissionCheck), tt.args.filter, tt.want, ) @@ -278,13 +346,14 @@ func TestSetDomainPrimary(t *testing.T) { func TestCommandSide_AddOrgDomain(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - orgID string - domain string - claimedUserIDs []string + ctx context.Context + orgID string + domain string + claimedUserIDs []string + permissionCheck OrganizationPermissionCheck } type res struct { want *domain.ObjectDetails @@ -299,9 +368,7 @@ func TestCommandSide_AddOrgDomain(t *testing.T) { { name: "invalid domain, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -313,8 +380,13 @@ func TestCommandSide_AddOrgDomain(t *testing.T) { { name: "domain already exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -343,8 +415,13 @@ func TestCommandSide_AddOrgDomain(t *testing.T) { { name: "domain add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -382,13 +459,76 @@ func TestCommandSide_AddOrgDomain(t *testing.T) { }, }, }, + { + name: "domain add (with permission check), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "name", + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + domain: "domain.ch", + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "no permission, error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + domain: "domain.ch", + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.AddOrgDomain(tt.args.ctx, tt.args.orgID, tt.args.domain, tt.args.claimedUserIDs) + got, err := r.AddOrgDomain(tt.args.ctx, tt.args.orgID, tt.args.domain, tt.args.claimedUserIDs, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } @@ -404,12 +544,13 @@ func TestCommandSide_AddOrgDomain(t *testing.T) { func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore secretGenerator crypto.Generator } type args struct { - ctx context.Context - domain *domain.OrgDomain + ctx context.Context + domain *domain.OrgDomain + permissionCheck OrganizationPermissionCheck } type res struct { wantToken string @@ -425,9 +566,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { { name: "invalid domain, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -444,9 +583,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { { name: "missing aggregateid, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -461,9 +598,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { { name: "invalid validation type, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -481,8 +616,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { { name: "domain not exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -510,8 +644,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { { name: "domain already verified, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -551,8 +684,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { { name: "add dns validation, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -601,8 +733,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { { name: "add http validation, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -648,14 +779,85 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { wantURL: "https://domain.ch/.well-known/zitadel-challenge/a.txt", }, }, + { + name: "add validation (with permission check), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + ), + ), + ), + expectPush( + org.NewDomainVerificationAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeHTTP, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeHTTP, + }, + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + res: res{ + wantToken: "a", + wantURL: "https://domain.ch/.well-known/zitadel-challenge/a.txt", + }, + }, + { + name: "add validation (no permission), error", + fields: fields{ + eventstore: expectEventstore(), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeHTTP, + }, + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), domainVerificationGenerator: tt.fields.secretGenerator, } - token, url, err := r.GenerateOrgDomainValidation(tt.args.ctx, tt.args.domain) + token, url, err := r.GenerateOrgDomainValidation(tt.args.ctx, tt.args.domain, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } @@ -679,9 +881,10 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { domainValidationFunc func(domain, token, verifier string, checkType http.CheckType) error } type args struct { - ctx context.Context - domain *domain.OrgDomain - claimedUserIDs []string + ctx context.Context + domain *domain.OrgDomain + claimedUserIDs []string + permissionCheck OrganizationPermissionCheck } type res struct { want *domain.ObjectDetails @@ -1172,6 +1375,86 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { }, }, }, + { + name: "domain verification (with permission check), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerificationAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeDNS, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + ), + expectPush( + org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + ), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + domainValidationFunc: validDomainVerification, + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "domain verification (no permission), error", + fields: fields{ + eventstore: expectEventstore(), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + domainValidationFunc: validDomainVerification, + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1182,7 +1465,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { domainVerificationValidator: tt.fields.domainValidationFunc, idGenerator: tt.fields.idGenerator, } - got, err := r.ValidateOrgDomain(http.WithRequestedHost(tt.args.ctx, "zitadel.ch"), tt.args.domain, tt.args.claimedUserIDs) + got, err := r.ValidateOrgDomain(http.WithRequestedHost(tt.args.ctx, "zitadel.ch"), tt.args.domain, tt.args.claimedUserIDs, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } @@ -1383,11 +1666,12 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) { func TestCommandSide_RemoveOrgDomain(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - domain *domain.OrgDomain + ctx context.Context + domain *domain.OrgDomain + permissionCheck OrganizationPermissionCheck } type res struct { want *domain.ObjectDetails @@ -1402,9 +1686,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { { name: "invalid domain, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -1421,9 +1703,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { { name: "missing aggregateid, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -1438,8 +1718,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { { name: "domain not exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1467,8 +1746,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { { name: "remove verified domain, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1513,8 +1791,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { { name: "remove domain, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1555,8 +1832,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { { name: "remove verified domain, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1600,13 +1876,80 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { }, }, }, + { + name: "remove verified domain (with permission check), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", + ), + ), + ), + expectPush( + org.NewDomainRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "domain.ch", true, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove verified domain (no permission), error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.RemoveOrgDomain(tt.args.ctx, tt.args.domain) + got, err := r.RemoveOrgDomain(tt.args.ctx, tt.args.domain, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/org_metadata.go b/internal/command/org_metadata.go index e564bb264..3f9637b76 100644 --- a/internal/command/org_metadata.go +++ b/internal/command/org_metadata.go @@ -32,10 +32,15 @@ func (c *Commands) SetOrgMetadata(ctx context.Context, orgID string, metadata *d return writeModelToOrgMetadata(setMetadata), nil } -func (c *Commands) BulkSetOrgMetadata(ctx context.Context, orgID string, metadatas ...*domain.Metadata) (_ *domain.ObjectDetails, err error) { +func (c *Commands) BulkSetOrgMetadata(ctx context.Context, orgID string, permissionCheck OrganizationPermissionCheck, metadatas ...*domain.Metadata) (_ *domain.ObjectDetails, err error) { if len(metadatas) == 0 { return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData") } + if permissionCheck != nil { + if err := permissionCheck(ctx, orgID); err != nil { + return nil, err + } + } err = c.checkOrgExists(ctx, orgID) if err != nil { return nil, err @@ -108,10 +113,15 @@ func (c *Commands) RemoveOrgMetadata(ctx context.Context, orgID, metadataKey str return writeModelToObjectDetails(&removeMetadata.WriteModel), nil } -func (c *Commands) BulkRemoveOrgMetadata(ctx context.Context, orgID string, metadataKeys ...string) (_ *domain.ObjectDetails, err error) { +func (c *Commands) BulkRemoveOrgMetadata(ctx context.Context, orgID string, permissionCheck OrganizationPermissionCheck, metadataKeys ...string) (_ *domain.ObjectDetails, err error) { if len(metadataKeys) == 0 { return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mw2d", "Errors.Metadata.NoData") } + if permissionCheck != nil { + if err := permissionCheck(ctx, orgID); err != nil { + return nil, err + } + } err = c.checkOrgExists(ctx, orgID) if err != nil { return nil, err diff --git a/internal/command/org_metadata_test.go b/internal/command/org_metadata_test.go index 667263007..ba1954b83 100644 --- a/internal/command/org_metadata_test.go +++ b/internal/command/org_metadata_test.go @@ -144,13 +144,14 @@ func TestCommandSide_SetOrgMetadata(t *testing.T) { func TestCommandSide_BulkSetOrgMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { - ctx context.Context - orgID string - metadataList []*domain.Metadata + ctx context.Context + orgID string + permissionCheck OrganizationPermissionCheck + metadataList []*domain.Metadata } ) type res struct { @@ -166,9 +167,7 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) { { name: "empty meta data list, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -181,8 +180,7 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) { { name: "org not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -201,8 +199,7 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -228,8 +225,7 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) { { name: "add metadata, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -266,13 +262,72 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) { }, }, }, + { + name: "add metadata (with permission check), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "ZITADEL", + ), + ), + ), + expectPush( + org.NewMetadataSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "key", + []byte("value"), + ), + org.NewMetadataSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "key1", + []byte("value1"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + metadataList: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + {Key: "key1", Value: []byte("value1")}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add metadata (no permission), error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + metadataList: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + {Key: "key1", Value: []byte("value1")}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.BulkSetOrgMetadata(tt.args.ctx, tt.args.orgID, tt.args.metadataList...) + got, err := r.BulkSetOrgMetadata(tt.args.ctx, tt.args.orgID, tt.args.permissionCheck, tt.args.metadataList...) if tt.res.err == nil { assert.NoError(t, err) } @@ -428,13 +483,14 @@ func TestCommandSide_OrgRemoveMetadata(t *testing.T) { func TestCommandSide_BulkRemoveOrgMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { - ctx context.Context - orgID string - metadataList []string + ctx context.Context + orgID string + permissionCheck OrganizationPermissionCheck + metadataList []string } ) type res struct { @@ -450,9 +506,7 @@ func TestCommandSide_BulkRemoveOrgMetadata(t *testing.T) { { name: "empty meta data list, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -465,8 +519,7 @@ func TestCommandSide_BulkRemoveOrgMetadata(t *testing.T) { { name: "org not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -482,8 +535,7 @@ func TestCommandSide_BulkRemoveOrgMetadata(t *testing.T) { { name: "remove metadata keys not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -515,8 +567,7 @@ func TestCommandSide_BulkRemoveOrgMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -555,8 +606,7 @@ func TestCommandSide_BulkRemoveOrgMetadata(t *testing.T) { { name: "remove metadata, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -604,13 +654,80 @@ func TestCommandSide_BulkRemoveOrgMetadata(t *testing.T) { }, }, }, + { + name: "remove metadata (with permission check), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "ZITADEL", + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewMetadataSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "key", + []byte("value"), + ), + ), + eventFromEventPusher( + org.NewMetadataSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "key1", + []byte("value1"), + ), + ), + ), + expectPush( + org.NewMetadataRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "key", + ), + org.NewMetadataRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "key1", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + metadataList: []string{"key", "key1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove metadata (no permission), ok", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + metadataList: []string{"key", "key1"}, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.BulkRemoveOrgMetadata(tt.args.ctx, tt.args.orgID, tt.args.metadataList...) + got, err := r.BulkRemoveOrgMetadata(tt.args.ctx, tt.args.orgID, tt.args.permissionCheck, tt.args.metadataList...) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/org_test.go b/internal/command/org_test.go index a2c271387..9d37eb927 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -463,9 +463,10 @@ func TestCommandSide_ChangeOrg(t *testing.T) { eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - orgID string - name string + ctx context.Context + orgID string + name string + permissionCheck OrganizationPermissionCheck } type res struct { err func(error) bool @@ -727,13 +728,76 @@ func TestCommandSide_ChangeOrg(t *testing.T) { }, res: res{}, }, + { + name: "change org name (with permission check), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org"), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.zitadel.ch"), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.zitadel.ch"), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org.zitadel.ch"), + ), + ), + expectPush( + org.NewOrgChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, "org", "ORG", + ), + ), + ), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), + orgID: "org1", + name: "ORG", + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + res: res{}, + }, + { + name: "change org name (no permission), error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), + orgID: "org1", + name: "ORG", + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore(t), } - _, err := r.ChangeOrg(tt.args.ctx, tt.args.orgID, tt.args.name) + _, err := r.ChangeOrg(tt.args.ctx, tt.args.orgID, tt.args.name, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } @@ -751,8 +815,9 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { iamDomain string } type args struct { - ctx context.Context - orgID string + ctx context.Context + orgID string + permissionCheck OrganizationPermissionCheck } type res struct { want *domain.Org @@ -855,6 +920,45 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { }, res: res{}, }, + { + name: "deactivate org (with permission check)", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), + expectPush( + org.NewOrgDeactivatedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + res: res{}, + }, + { + name: "deactivate org (no permission)", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -862,7 +966,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } - _, err := r.DeactivateOrg(tt.args.ctx, tt.args.orgID) + _, err := r.DeactivateOrg(tt.args.ctx, tt.args.orgID, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } @@ -880,8 +984,9 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { iamDomain string } type args struct { - ctx context.Context - orgID string + ctx context.Context + orgID string + permissionCheck OrganizationPermissionCheck } type res struct { want *domain.Org @@ -989,6 +1094,49 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { }, res: res{}, }, + { + name: "reactivate org (with permission check)", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org"), + ), + eventFromEventPusher( + org.NewOrgDeactivatedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate), + ), + ), + expectPush( + org.NewOrgReactivatedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + res: res{}, + }, + { + name: "reactivate org (with permission check)", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -996,7 +1144,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } - _, err := r.ReactivateOrg(tt.args.ctx, tt.args.orgID) + _, err := r.ReactivateOrg(tt.args.ctx, tt.args.orgID, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } @@ -1013,8 +1161,10 @@ func TestCommandSide_RemoveOrg(t *testing.T) { idGenerator id.Generator } type args struct { - ctx context.Context - orgID string + ctx context.Context + orgID string + permissionCheck OrganizationPermissionCheck + mustExist bool } type res struct { err func(error) bool @@ -1064,7 +1214,24 @@ func TestCommandSide_RemoveOrg(t *testing.T) { }, }, { - name: "org not found, error", + name: "org not found, must exist, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), // zitadel project check + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + mustExist: true, + }, + res: res{ + err: zerrors.IsNotFound, + }, + }, + { + name: "org not found, ok", fields: fields{ eventstore: expectEventstore( expectFilter(), // zitadel project check @@ -1075,9 +1242,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { ctx: context.Background(), orgID: "org1", }, - res: res{ - err: zerrors.IsNotFound, - }, + res: res{}, }, { name: "push failed, error", @@ -1252,6 +1417,63 @@ func TestCommandSide_RemoveOrg(t *testing.T) { }, res: res{}, }, + + { + name: "remove org (with permission check)", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), // zitadel project check + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org"), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter(), + expectFilterOrganizationSettings("org1", false, false), + expectFilter(), + expectFilter(), + expectFilter(), + expectPush( + org.NewOrgRemovedEvent( + context.Background(), &org.NewAggregate("org1").Aggregate, "org", []string{}, false, []string{}, []*domain.UserIDPLink{}, []string{}, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckAllowed(), + }, + res: res{}, + }, + + { + name: "remove org (no permission)", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1259,7 +1481,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } - _, err := r.RemoveOrg(tt.args.ctx, tt.args.orgID) + _, err := r.RemoveOrg(tt.args.ctx, tt.args.orgID, tt.args.permissionCheck, tt.args.mustExist) if tt.res.err == nil { assert.NoError(t, err) } @@ -1281,6 +1503,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ctx context.Context setupOrg *OrgSetup allowInitialMail bool + permissionCheck OrganizationPermissionCheck userIDs []string } type res struct { @@ -1821,6 +2044,41 @@ func TestCommandSide_SetUpOrg(t *testing.T) { }, }, }, + { + name: "no permission, error", + fields: fields{ + eventstore: expectEventstore(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"), + newCode: mockEncryptedCode("userinit", time.Hour), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + Admins: []*OrgSetupAdmin{ + { + Machine: &AddMachine{ + Machine: &Machine{ + Username: "username", + Name: "name", + Description: "description", + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + Pat: &AddPat{ + ExpirationDate: testNow.Add(time.Hour), + Scopes: []string{openid.ScopeOpenID}, + }, + }, + }, + }, + }, + permissionCheck: newMockOrganizationPermissionCheckNotAllowed(), + }, + res: res{ + err: zerrors.ThrowPermissionDenied(nil, "", "Errors.PermissionDenied"), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1835,7 +2093,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { }, }, } - got, err := r.SetUpOrg(tt.args.ctx, tt.args.setupOrg, tt.args.allowInitialMail, tt.args.userIDs...) + got, err := r.SetUpOrg(tt.args.ctx, tt.args.setupOrg, tt.args.allowInitialMail, tt.args.permissionCheck, tt.args.userIDs...) assert.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.createdOrg, got) }) diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go index 42445c6b8..b7dc03364 100644 --- a/internal/command/permission_checks.go +++ b/internal/command/permission_checks.go @@ -161,6 +161,18 @@ func (c *Commands) NewPermissionCheckUserGrantDelete(ctx context.Context) UserGr return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantDelete) } +func (c *Commands) CheckPermissionOrganizationCreate(ctx context.Context, organizationID string) error { + return c.newPermissionCheck(ctx, domain.PermissionOrganizationWrite, org.AggregateType)(organizationID, organizationID) +} + +func (c *Commands) CheckPermissionOrganizationWrite(ctx context.Context, organizationID string) error { + return c.newPermissionCheck(ctx, domain.PermissionOrganizationWrite, org.AggregateType)(organizationID, organizationID) +} + +func (c *Commands) CheckPermissionOrganizationDelete(ctx context.Context, organizationID string) error { + return c.newPermissionCheck(ctx, domain.PermissionOrganizationDelete, org.AggregateType)(organizationID, organizationID) +} + func (c *Commands) checkPermissionCreateGroup(ctx context.Context, resourceOwner, groupID string) error { return c.newPermissionCheck(ctx, domain.PermissionGroupCreate, group.AggregateType)(resourceOwner, groupID) } diff --git a/internal/domain/permission.go b/internal/domain/permission.go index 092611b6d..227f67c14 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -37,6 +37,8 @@ const ( PermissionSessionLink = "session.link" PermissionSessionDelete = "session.delete" PermissionOrgRead = "org.read" + PermissionOrganizationWrite = "org.write" + PermissionOrganizationDelete = "org.delete" PermissionIDPRead = "iam.idp.read" PermissionOrgIDPRead = "org.idp.read" PermissionProjectCreate = "project.create" diff --git a/internal/query/org_domain.go b/internal/query/org_domain.go index ed0dba9c1..042d39f3c 100644 --- a/internal/query/org_domain.go +++ b/internal/query/org_domain.go @@ -55,7 +55,7 @@ func NewOrgDomainVerifiedSearchQuery(verified bool) (SearchQuery, error) { return NewBoolQuery(OrgDomainIsVerifiedCol, verified) } -func (q *Queries) SearchOrgDomains(ctx context.Context, queries *OrgDomainSearchQueries, withOwnerRemoved bool) (domains *Domains, err error) { +func (q *Queries) SearchOrgDomains(ctx context.Context, queries *OrgDomainSearchQueries, withOwnerRemoved, withPermissionCheck bool) (domains *Domains, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -64,6 +64,11 @@ func (q *Queries) SearchOrgDomains(ctx context.Context, queries *OrgDomainSearch if !withOwnerRemoved { eq[OrgDomainOwnerRemovedCol.identifier()] = false } + if withPermissionCheck { + // We always use the permission v2 check and don't check the feature flag, since it's stable enough to work + // in this case and using the old checks only adds more latency, but no benefit. + query = orgDomainPermissionCheckV2(ctx, query, queries) + } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-ZRfj1", "Errors.Query.SQLStatement") @@ -176,3 +181,13 @@ var ( table: orgDomainsTable, } ) + +func orgDomainPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, queries *OrgDomainSearchQueries) sq.SelectBuilder { + join, args := PermissionClause( + ctx, + OrgDomainOrgIDCol, + domain.PermissionOrgRead, + SingleOrgPermissionOption(queries.Queries), + ) + return query.JoinClause(join, args...) +} diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index e67c7222c..992e2de17 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -10,6 +10,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + domain_pkg "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -113,7 +114,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo return metadata, err } -func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, orgID string, queries *OrgMetadataSearchQueries, withOwnerRemoved bool) (metadata *OrgMetadataList, err error) { +func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, orgID string, queries *OrgMetadataSearchQueries, withOwnerRemoved, withPermissionCheck bool) (metadata *OrgMetadataList, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -131,6 +132,11 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, eq[OrgMetadataOwnerRemovedCol.identifier()] = false } query, scan := prepareOrgMetadataListQuery() + if withPermissionCheck { + // We always use the permission v2 check and don't check the feature flag, since it's stable enough to work + // in this case and using the old checks only adds more latency, but no benefit. + query = orgMetadataPermissionCheckV2(ctx, query, queries) + } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatement") @@ -248,3 +254,13 @@ func prepareOrgMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*OrgMetad }, nil } } + +func orgMetadataPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, queries *OrgMetadataSearchQueries) sq.SelectBuilder { + join, args := PermissionClause( + ctx, + OrgMetadataOrgIDCol, + domain_pkg.PermissionOrgRead, + SingleOrgPermissionOption(queries.Queries), + ) + return query.JoinClause(join, args...) +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index e30843c66..0b2870491 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 f533f7cde..bfef940e6 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 1f23b85eb..0b2602f97 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 729350e1f..c09755e3f 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" @@ -123,7 +130,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.create" + permission: "authenticated" } http_response: { success_code: 201 @@ -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 0606dd357..11996c27f 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 43307214f..33a6264e6 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 @@ -125,7 +128,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.create" + permission: "authenticated" } }; @@ -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: @@ -160,7 +165,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } }; @@ -189,10 +194,12 @@ 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: - // - `iam.read` + // - `org.read` // // Deprecated: Use [ListOrganizations](/apis/resources/org_service_v2/organization-service-list-organizations.api.mdx) instead to list organizations. rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) { @@ -203,7 +210,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "iam.read"; + permission: "authenticated"; } }; @@ -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: @@ -228,7 +237,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.delete"; + permission: "authenticated"; } }; @@ -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: @@ -262,7 +273,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } }; @@ -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: @@ -293,7 +306,7 @@ service OrganizationService { }; option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.read" + permission: "authenticated" } }; @@ -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: @@ -318,7 +333,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } }; @@ -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: @@ -344,7 +361,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } }; @@ -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: @@ -376,7 +395,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.read" + permission: "authenticated" } }; @@ -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: @@ -401,7 +422,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } }; @@ -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: @@ -427,7 +450,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } }; @@ -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: @@ -458,7 +483,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } }; @@ -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: @@ -483,7 +510,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } }; @@ -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: @@ -509,7 +538,7 @@ service OrganizationService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "org.write" + permission: "authenticated" } };