From ae1a2e93c1e10a05d43762654ba8b4c8daed6a3d Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:27:53 +0200 Subject: [PATCH] feat(api): moving organization API resourced based (#9943) --- cmd/start/start.go | 2 +- docs/docusaurus.config.js | 8 + docs/sidebars.js | 13 + internal/api/grpc/instance/converter.go | 4 +- .../v2beta/integration_test/instance_test.go | 5 +- .../v2beta/integration_test/query_test.go | 5 +- internal/api/grpc/management/org_converter.go | 4 +- internal/api/grpc/management/user.go | 4 + internal/api/grpc/metadata/v2beta/metadata.go | 49 + internal/api/grpc/object/v2beta/converter.go | 55 + internal/api/grpc/org/v2beta/helper.go | 256 +++ .../org/v2beta/integration_test/org_test.go | 1815 ++++++++++++++++- internal/api/grpc/org/v2beta/org.go | 238 ++- internal/api/grpc/org/v2beta/org_test.go | 45 +- internal/api/grpc/org/v2beta/server.go | 4 + internal/query/org_metadata.go | 1 - proto/zitadel/admin.proto | 28 +- proto/zitadel/management.proto | 258 +-- proto/zitadel/metadata/v2beta/metadata.proto | 57 + proto/zitadel/org/v2beta/org.proto | 169 ++ proto/zitadel/org/v2beta/org_service.proto | 825 +++++++- 21 files changed, 3542 insertions(+), 303 deletions(-) create mode 100644 internal/api/grpc/metadata/v2beta/metadata.go create mode 100644 internal/api/grpc/org/v2beta/helper.go create mode 100644 proto/zitadel/metadata/v2beta/metadata.proto create mode 100644 proto/zitadel/org/v2beta/org.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index af76b29e99..2fc1fb8413 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -470,7 +470,7 @@ func startAPIs( if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, org_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil { diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c161d38d9f..43830eafd0 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -342,6 +342,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + org_v2beta: { + specPath: ".artifacts/openapi/zitadel/org/v2beta/org_service.swagger.json", + outputDir: "docs/apis/resources/org_service_v2beta", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, project_v2beta: { specPath: ".artifacts/openapi/zitadel/project/v2beta/project_service.swagger.json", outputDir: "docs/apis/resources/project_service_v2", diff --git a/docs/sidebars.js b/docs/sidebars.js index b7a399ecf1..f9b97703e5 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -10,6 +10,7 @@ const sidebar_api_oidc_service_v2 = require("./docs/apis/resources/oidc_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 @@ -791,6 +792,18 @@ 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 API is intended to manage organizations for ZITADEL. \n", + }, + items: sidebar_api_org_service_v2beta, + }, { type: "category", label: "Identity Provider", diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index 4094da4a77..b894a064ff 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -28,7 +28,7 @@ func InstanceToPb(instance *query.Instance) *instance_pb.Instance { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } @@ -44,7 +44,7 @@ func InstanceDetailToPb(instance *query.Instance) *instance_pb.InstanceDetail { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } diff --git a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go index 5187bbc78d..ae277c6d13 100644 --- a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go +++ b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go @@ -9,10 +9,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/zitadel/internal/integration" - instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" ) func TestDeleteInstace(t *testing.T) { diff --git a/internal/api/grpc/instance/v2beta/integration_test/query_test.go b/internal/api/grpc/instance/v2beta/integration_test/query_test.go index 0828b006e3..e59a16a932 100644 --- a/internal/api/grpc/instance/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/instance/v2beta/integration_test/query_test.go @@ -11,12 +11,13 @@ import ( "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "github.com/zitadel/zitadel/internal/integration" filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" "github.com/zitadel/zitadel/pkg/grpc/object/v2" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) func TestGetInstance(t *testing.T) { diff --git a/internal/api/grpc/management/org_converter.go b/internal/api/grpc/management/org_converter.go index 879b5e0763..03de84cdf4 100644 --- a/internal/api/grpc/management/org_converter.go +++ b/internal/api/grpc/management/org_converter.go @@ -26,7 +26,7 @@ func ListOrgDomainsRequestToModel(req *mgmt_pb.ListOrgDomainsRequest) (*query.Or Limit: limit, Asc: asc, }, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting Queries: queries, }, nil } @@ -89,7 +89,7 @@ func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembe Offset: offset, Limit: limit, Asc: asc, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting }, Queries: queries, }, diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 5b82eb5afe..f318051e63 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -901,6 +901,7 @@ func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHuman Details: obj_grpc.ToListDetails(res.Count, res.Sequence, res.LastRun), }, nil } + func (s *Server) RemoveHumanLinkedIDP(ctx context.Context, req *mgmt_pb.RemoveHumanLinkedIDPRequest) (*mgmt_pb.RemoveHumanLinkedIDPResponse, error) { objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveHumanLinkedIDPRequestToDomain(ctx, req)) if err != nil { @@ -947,18 +948,21 @@ func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingI } return &command.CascadingIAMMembership{IAMID: membership.IAMID} } + func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership { if membership == nil { return nil } return &command.CascadingOrgMembership{OrgID: membership.OrgID} } + func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership { if membership == nil { return nil } return &command.CascadingProjectMembership{ProjectID: membership.ProjectID} } + func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership { if membership == nil { return nil diff --git a/internal/api/grpc/metadata/v2beta/metadata.go b/internal/api/grpc/metadata/v2beta/metadata.go new file mode 100644 index 0000000000..57da21dfd2 --- /dev/null +++ b/internal/api/grpc/metadata/v2beta/metadata.go @@ -0,0 +1,49 @@ +package metadata + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta" +) + +// code in this file is copied from internal/api/grpc/metadata/metadata.go + +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.MetadataQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgMetadataQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgMetadataQueryToQuery(metadataQuery *meta_pb.MetadataQuery) (query.SearchQuery, error) { + switch q := metadataQuery.Query.(type) { + case *meta_pb.MetadataQuery_KeyQuery: + return query.NewOrgMetadataKeySearchQuery(q.KeyQuery.Key, v2beta_object.TextMethodToQuery(q.KeyQuery.Method)) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/object/v2beta/converter.go b/internal/api/grpc/object/v2beta/converter.go index 9b14bb677a..73d5f18843 100644 --- a/internal/api/grpc/object/v2beta/converter.go +++ b/internal/api/grpc/object/v2beta/converter.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org_pb "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { @@ -34,6 +35,7 @@ func ToListDetails(response query.SearchResponse) *object.ListDetails { return details } + func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) { if query == nil { return 0, 0, false @@ -73,3 +75,56 @@ func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison { return -1 } } + +func ListQueryToModel(query *object.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainsToPb(domains []*query.Domain) []*org_pb.Domain { + d := make([]*org_pb.Domain, len(domains)) + for i, domain := range domains { + d[i] = DomainToPb(domain) + } + return d +} + +func DomainToPb(d *query.Domain) *org_pb.Domain { + return &org_pb.Domain{ + OrganizationId: d.OrgID, + DomainName: d.Domain, + IsVerified: d.IsVerified, + IsPrimary: d.IsPrimary, + ValidationType: DomainValidationTypeFromModel(d.ValidationType), + } +} + +func DomainValidationTypeFromModel(validationType domain.OrgDomainValidationType) org_pb.DomainValidationType { + switch validationType { + case domain.OrgDomainValidationTypeDNS: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS + case domain.OrgDomainValidationTypeHTTP: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP + case domain.OrgDomainValidationTypeUnspecified: + // added to please golangci-lint + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + default: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + } +} + +func DomainValidationTypeToDomain(validationType org_pb.DomainValidationType) domain.OrgDomainValidationType { + switch validationType { + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP: + return domain.OrgDomainValidationTypeHTTP + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS: + return domain.OrgDomainValidationTypeDNS + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED: + // added to please golangci-lint + return domain.OrgDomainValidationTypeUnspecified + default: + return domain.OrgDomainValidationTypeUnspecified + } +} diff --git a/internal/api/grpc/org/v2beta/helper.go b/internal/api/grpc/org/v2beta/helper.go new file mode 100644 index 0000000000..39bad0dae2 --- /dev/null +++ b/internal/api/grpc/org/v2beta/helper.go @@ -0,0 +1,256 @@ +package org + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + // TODO fix below + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" +) + +// NOTE: most of this code is copied from `internal/api/grpc/admin/*`, as we will eventually axe the previous versons of the API, +// we will have code duplication until then + +func listOrgRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationsRequest) (*query.OrgSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := OrgQueriesToModel(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + SortingColumn: FieldNameToOrgColumn(request.SortingColumn), + Asc: asc, + }, + Queries: queries, + }, nil +} + +func OrganizationViewToPb(org *query.Org) *v2beta_org.Organization { + return &v2beta_org.Organization{ + Id: org.ID, + State: OrgStateToPb(org.State), + Name: org.Name, + PrimaryDomain: org.Domain, + CreationDate: timestamppb.New(org.CreationDate), + ChangedDate: timestamppb.New(org.ChangeDate), + } +} + +func OrgStateToPb(state domain.OrgState) v2beta_org.OrgState { + switch state { + case domain.OrgStateActive: + return v2beta_org.OrgState_ORG_STATE_ACTIVE + case domain.OrgStateInactive: + return v2beta_org.OrgState_ORG_STATE_INACTIVE + case domain.OrgStateRemoved: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_REMOVED + case domain.OrgStateUnspecified: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + default: + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + } +} + +func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.CreateOrganizationResponse, err error) { + admins := make([]*org.CreatedAdmin, len(createdOrg.CreatedAdmins)) + for i, admin := range createdOrg.CreatedAdmins { + admins[i] = &org.CreatedAdmin{ + UserId: admin.ID, + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + } + } + return &org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), + Id: createdOrg.ObjectDetails.ResourceOwner, + CreatedAdmins: admins, + }, nil +} + +func OrgViewsToPb(orgs []*query.Org) []*v2beta_org.Organization { + o := make([]*v2beta_org.Organization, len(orgs)) + for i, org := range orgs { + o[i] = OrganizationViewToPb(org) + } + return o +} + +func OrgQueriesToModel(queries []*v2beta_org.OrganizationSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgQueryToModel(apiQuery *v2beta_org.OrganizationSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *v2beta_org.OrganizationSearchFilter_DomainFilter: + return query.NewOrgVerifiedDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainFilter.Method), q.DomainFilter.Domain) + case *v2beta_org.OrganizationSearchFilter_NameFilter: + return query.NewOrgNameSearchQuery(v2beta_object.TextMethodToQuery(q.NameFilter.Method), q.NameFilter.Name) + case *v2beta_org.OrganizationSearchFilter_StateFilter: + return query.NewOrgStateSearchQuery(OrgStateToDomain(q.StateFilter.State)) + case *v2beta_org.OrganizationSearchFilter_IdFilter: + return query.NewOrgIDSearchQuery(q.IdFilter.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func OrgStateToDomain(state v2beta_org.OrgState) domain.OrgState { + switch state { + case v2beta_org.OrgState_ORG_STATE_ACTIVE: + return domain.OrgStateActive + case v2beta_org.OrgState_ORG_STATE_INACTIVE: + return domain.OrgStateInactive + case v2beta_org.OrgState_ORG_STATE_REMOVED: + // added to please golangci-lint + return domain.OrgStateRemoved + case v2beta_org.OrgState_ORG_STATE_UNSPECIFIED: + fallthrough + default: + return domain.OrgStateUnspecified + } +} + +func FieldNameToOrgColumn(fieldName v2beta_org.OrgFieldName) query.Column { + switch fieldName { + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_NAME: + return query.OrgColumnName + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_CREATION_DATE: + return query.OrgColumnCreationDate + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_UNSPECIFIED: + return query.Column{} + default: + return query.Column{} + } +} + +func ListOrgDomainsRequestToModel(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: //TODO: sorting + Queries: queries, + }, nil +} + +func ListQueryToModel(query *v2beta.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainQueriesToModel(queries []*v2beta_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 *v2beta_org.DomainSearchFilter) (query.SearchQuery, error) { + switch q := searchQuery.Filter.(type) { + case *v2beta_org.DomainSearchFilter_DomainNameFilter: + return query.NewOrgDomainDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainNameFilter.Method), q.DomainNameFilter.Name) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Ags89", "List.Query.Invalid") + } +} + +func RemoveOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.DeleteOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func GenerateOrgDomainValidationRequestToDomain(ctx context.Context, req *v2beta_org.GenerateOrganizationDomainValidationRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + ValidationType: v2beta_object.DomainValidationTypeToDomain(req.Type), + } +} + +func ValidateOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.VerifyOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func BulkSetOrgMetadataToDomain(req *v2beta_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 ListOrgMetadataToDomain(systemDefaults systemdefaults.SystemDefaults, request *v2beta_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.Filter) + if err != nil { + return nil, err + } + return &query.OrgMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil +} 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 a2b2bf6047..4e0ec26121 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -4,7 +4,9 @@ package org_test import ( "context" + "errors" "os" + "strings" "testing" "time" @@ -14,7 +16,10 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/admin" + v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + 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" ) @@ -22,7 +27,7 @@ import ( var ( CTX context.Context Instance *integration.Instance - Client org.OrganizationServiceClient + Client v2beta_org.OrganizationServiceClient User *user.AddHumanUserResponse ) @@ -40,20 +45,21 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddOrganization(t *testing.T) { +func TestServer_CreateOrganization(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) tests := []struct { name string ctx context.Context - req *org.AddOrganizationRequest - want *org.AddOrganizationResponse + req *v2beta_org.CreateOrganizationRequest + id string + want *v2beta_org.CreateOrganizationResponse wantErr bool }{ { name: "missing permission", - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &org.AddOrganizationRequest{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &v2beta_org.CreateOrganizationRequest{ Name: "name", Admins: nil, }, @@ -62,7 +68,7 @@ func TestServer_AddOrganization(t *testing.T) { { name: "empty name", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: "", Admins: nil, }, @@ -71,34 +77,22 @@ func TestServer_AddOrganization(t *testing.T) { { name: "invalid admin type", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ {}, }, }, wantErr: true, }, - { - name: "no admin, custom org ID", - ctx: CTX, - req: &org.AddOrganizationRequest{ - Name: gofakeit.AppName(), - OrgId: gu.Ptr("custom-org-ID"), - }, - want: &org.AddOrganizationResponse{ - OrganizationId: "custom-org-ID", - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, - }, - }, { name: "admin with init", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -115,9 +109,9 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - OrganizationId: integration.NotEmpty, - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &v2beta_org.CreateOrganizationResponse{ + Id: integration.NotEmpty, + CreatedAdmins: []*v2beta_org.CreatedAdmin{ { UserId: integration.NotEmpty, EmailCode: gu.Ptr(integration.NotEmpty), @@ -129,14 +123,14 @@ func TestServer_AddOrganization(t *testing.T) { { name: "existing user and new human with idp", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, }, { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -160,8 +154,8 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &v2beta_org.CreateOrganizationResponse{ + CreatedAdmins: []*v2beta_org.CreatedAdmin{ // a single admin is expected, because the first provided already exists { UserId: integration.NotEmpty, @@ -169,25 +163,36 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, + { + name: "create with ID", + ctx: CTX, + id: "custom_id", + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Id: gu.Ptr("custom_id"), + }, + want: &v2beta_org.CreateOrganizationResponse{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.AddOrganization(tt.ctx, tt.req) + got, err := Client.CreateOrganization(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) + if tt.id != "" { + require.Equal(t, tt.id, got.Id) + } + // check details - assert.NotZero(t, got.GetDetails().GetSequence()) - gotCD := got.GetDetails().GetChangeDate().AsTime() + gotCD := got.GetCreationDate().AsTime() now := time.Now() assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) - assert.NotEmpty(t, got.GetDetails().GetResourceOwner()) // organization id must be the same as the resourceOwner - assert.Equal(t, got.GetDetails().GetResourceOwner(), got.GetOrganizationId()) // check the admins require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins())) @@ -199,7 +204,1739 @@ func TestServer_AddOrganization(t *testing.T) { } } -func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) { +func TestServer_UpdateOrganization(t *testing.T) { + orgs, orgsName, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + orgName := orgsName[0] + + 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.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: "new org name", + }, + }, + { + name: "update org with same name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: orgName, + }, + }, + { + name: "update org with non existent org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "non existant org id", + // Name: "", + }, + wantErr: true, + }, + { + name: "update org with no id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "", + Name: orgName, + }, + 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_ListOrganizations(t *testing.T) { + testStartTimestamp := time.Now() + ListOrgIinstance := integration.NewInstance(CTX) + listOrgIAmOwnerCtx := ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + listOrgClient := ListOrgIinstance.Client.OrgV2beta + + noOfOrgs := 3 + orgs, orgsName, err := createOrgs(listOrgIAmOwnerCtx, listOrgClient, noOfOrgs) + if err != nil { + require.NoError(t, err) + return + } + + // deactivat org[1] + _, err = listOrgClient.DeactivateOrganization(listOrgIAmOwnerCtx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgs[1].Id, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + query []*v2beta_org.OrganizationSearchFilter + want []*v2beta_org.Organization + err error + }{ + { + name: "list organizations, without required permissions", + ctx: ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeNoPermission), + err: errors.New("membership not found"), + }, + { + name: "list organizations happy path, no filter", + ctx: listOrgIAmOwnerCtx, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by id happy path", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by state active", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_ACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by state inactive", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_INACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by id bad id", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: "bad id", + }, + }, + }, + }, + }, + { + name: "list organizations specify org name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: orgsName[1], + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return orgsName[1][1 : len(orgsName[1])-2] + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return strings.ToUpper(orgsName[1][1 : len(orgsName[1])-2]) + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + listOrgRes, err := listOrgClient.ListOrganizations(listOrgIAmOwnerCtx, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + }) + require.NoError(t, err) + domain := listOrgRes.Organizations[0].PrimaryDomain + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToLower(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToUpper(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := listOrgClient.ListOrganizations(tt.ctx, &v2beta_org.ListOrganizationsRequest{ + Filter: tt.query, + }) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + return + } + require.NoError(ttt, err) + + require.Equal(ttt, uint64(len(tt.want)), got.Pagination.GetTotalResult()) + + foundOrgs := 0 + for _, got := range got.Organizations { + for _, org := range tt.want { + + // 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 + } + } + } + require.Equal(ttt, len(tt.want), foundOrgs) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + createOrgFunc func() string + req *v2beta_org.DeleteOrganizationRequest + want *v2beta_org.DeleteOrganizationResponse + dontCheckTime bool + err error + }{ + { + name: "delete org no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + err: errors.New("membership not found"), + }, + { + name: "delete org happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + }, + { + name: "delete already deleted org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + // delete org + _, err = Client.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{Id: orgs[0].Id}) + require.NoError(t, err) + + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + dontCheckTime: true, + }, + { + name: "delete non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.DeleteOrganizationRequest{ + Id: "non existent org id", + }, + dontCheckTime: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.createOrgFunc != nil { + tt.req.Id = 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.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + // deactivate non existent organization + _, err := Client.DeactivateOrganization(ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") + + // reactivate non existent organization + _, err = Client.ActivateOrganization(ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: "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, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: 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, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "Activate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + 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, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + 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, &v2beta_org.ActivateOrganizationRequest{ + Id: 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, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + return orgId + }, + }, + { + name: "Deactivate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + 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, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: 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, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[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, &v2beta_org.DeactivateOrganizationRequest{ + Id: 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", + domain: gofakeit.URL(), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + return orgId + }, + }, + { + name: "add org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 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(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, 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", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: should return a error + err: nil, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + 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)) + } + } +} + +func TestServer_ListOrganizationDomains(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "list org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + 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)) + + return orgId + }, + }, + } + + 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(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == tt.domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for adding domain") + + } +} + +func TestServer_DeleteOerganizationDomain(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "delete org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + 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(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "delete org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 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(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_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", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: + err: errors.New("Domain doesn't exist on organization"), + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + _, err := Client.DeleteOrganizationDomain(CTX, &v2beta_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_AddListDeleteOrganizationDomain(t *testing.T) { + tests := []struct { + name string + testFunc func() + }{ + { + name: "add org domain, re-add org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + // ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + domain := gofakeit.URL() + // 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)) + + // 3. re-add domain + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for adding already existing domain + // require.NoError(t, err) + require.Contains(t, err.Error(), "Errors.Already.Exists") + // check details + // gotCD = addOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 4. check domain is added + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, + }, + { + name: "add org domain, delete org domain, re-delete org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 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)) + + // 2. delete organisation domain + deleteOrgDomainRes, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD = deleteOrgDomainRes.GetDeletionDate().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(t *assert.CollectT) { + // 3. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // 4. redelete organisation domain + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for deleting org domain already deleted + // require.NoError(t, err) + require.Contains(t, err.Error(), "Domain doesn't exist on organization") + // check details + // gotCD = deleteOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 5. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.testFunc() + }) + } +} + +func TestServer_ValidateOrganizationDomain(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + _, 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 := gofakeit.URL() + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.GenerateOrganizationDomainValidationRequest + err error + }{ + { + name: "validate org http happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + }, + { + name: "validate org http non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org dns happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + }, + { + name: "validate org dns non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org non existnetn domain", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: "non existent domain", + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("Domain doesn't exist on organization"), + }, + } + 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, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + key string + value string + err error + }{ + { + name: "set org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + key: "key1", + value: "value1", + }, + { + name: "set org metadata on non existant org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existant orgid", + key: "key2", + value: "value2", + err: errors.New("Organisation not found"), + }, + { + name: "update org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_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.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_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, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Metadata: []*v2beta_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, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }) + require.NoError(t, 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_ListOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + keyValuPars []struct { + key string + value string + } + }{ + { + name: "list org metadata happy path", + ctx: Instance.WithAuthorization(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) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "list multiple org metadata happy path", + ctx: Instance.WithAuthorization(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"), + }, + { + Key: "key4", + Value: []byte("value4"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + { + key: "key4", + value: "value4", + }, + }, + }, + { + name: "list org metadata for non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existent orgid", + keyValuPars: []struct{ key, value string }{}, + }, + } + 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) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + + foundMetadataCount := 0 + for _, kv := range tt.keyValuPars { + for _, res := range got.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.keyValuPars), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + metadataToDelete []struct { + key string + value string + } + metadataToRemain []struct { + key string + value string + } + err error + }{ + { + name: "delete org metadata happy path", + ctx: Instance.WithAuthorization(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) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "delete multiple org metadata happy path", + ctx: Instance.WithAuthorization(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) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + }, + }, + { + name: "delete some org metadata but not all", + ctx: Instance.WithAuthorization(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) + }, + 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.WithAuthorization(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.WithAuthorization(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"), + }, + } + 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 { + keys[i] = kvp.key + } + + // run delete + _, err = Client.DeleteOrganizationMetadata(tt.ctx, &v2beta_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, &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, 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) + }) + } +} + +func createOrgs(ctx context.Context, client v2beta_org.OrganizationServiceClient, noOfOrgs int) ([]*v2beta_org.CreateOrganizationResponse, []string, error) { + var err error + orgs := make([]*v2beta_org.CreateOrganizationResponse, noOfOrgs) + orgsName := make([]string, noOfOrgs) + + for i := range noOfOrgs { + orgName := gofakeit.Name() + orgsName[i] = orgName + orgs[i], err = client.CreateOrganization(ctx, + &v2beta_org.CreateOrganizationRequest{ + Name: orgName, + }, + ) + if err != nil { + return nil, nil, err + } + } + + return orgs, orgsName, nil +} + +func assertCreatedAdmin(t *testing.T, expected, got *v2beta_org.CreatedAdmin) { if expected.GetUserId() != "" { assert.NotEmpty(t, got.GetUserId()) } else { diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index 39730f827e..66198757cb 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -2,16 +2,23 @@ package org import ( "context" + "errors" + "google.golang.org/protobuf/types/known/timestamppb" + + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) -func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) { - orgSetup, err := addOrganizationRequestToCommand(request) +func (s *Server) CreateOrganization(ctx context.Context, request *v2beta_org.CreateOrganizationRequest) (*v2beta_org.CreateOrganizationResponse, error) { + orgSetup, err := createOrganizationRequestToCommand(request) if err != nil { return nil, err } @@ -22,8 +29,182 @@ func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizati return createdOrganizationToPb(createdOrg) } -func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) { - admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins()) +func (s *Server) UpdateOrganization(ctx context.Context, request *v2beta_org.UpdateOrganizationRequest) (*v2beta_org.UpdateOrganizationResponse, error) { + org, err := s.command.ChangeOrg(ctx, request.Id, request.Name) + if err != nil { + return nil, err + } + + return &v2beta_org.UpdateOrganizationResponse{ + ChangeDate: timestamppb.New(org.EventDate), + }, nil +} + +func (s *Server) ListOrganizations(ctx context.Context, request *v2beta_org.ListOrganizationsRequest) (*v2beta_org.ListOrganizationsResponse, error) { + queries, err := listOrgRequestToModel(s.systemDefaults, request) + if err != nil { + return nil, err + } + orgs, err := s.query.SearchOrgs(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &v2beta_org.ListOrganizationsResponse{ + Organizations: OrgViewsToPb(orgs.Orgs), + Pagination: &filter.PaginationResponse{ + TotalResult: orgs.Count, + AppliedLimit: uint64(request.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganization(ctx context.Context, request *v2beta_org.DeleteOrganizationRequest) (*v2beta_org.DeleteOrganizationResponse, error) { + details, err := s.command.RemoveOrg(ctx, request.Id) + if err != nil { + var notFoundError *zerrors.NotFoundError + if errors.As(err, ¬FoundError) { + return &v2beta_org.DeleteOrganizationResponse{}, nil + } + return nil, err + } + return &v2beta_org.DeleteOrganizationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) SetOrganizationMetadata(ctx context.Context, request *v2beta_org.SetOrganizationMetadataRequest) (*v2beta_org.SetOrganizationMetadataResponse, error) { + result, err := s.command.BulkSetOrgMetadata(ctx, request.OrganizationId, BulkSetOrgMetadataToDomain(request)...) + if err != nil { + return nil, err + } + return &org.SetOrganizationMetadataResponse{ + SetDate: timestamppb.New(result.EventDate), + }, nil +} + +func (s *Server) ListOrganizationMetadata(ctx context.Context, request *v2beta_org.ListOrganizationMetadataRequest) (*v2beta_org.ListOrganizationMetadataResponse, error) { + metadataQueries, err := ListOrgMetadataToDomain(s.systemDefaults, request) + if err != nil { + return nil, err + } + res, err := s.query.SearchOrgMetadata(ctx, true, request.OrganizationId, metadataQueries, false) + if err != nil { + return nil, err + } + return &v2beta_org.ListOrganizationMetadataResponse{ + Metadata: metadata.OrgMetadataListToPb(res.Metadata), + Pagination: &filter.PaginationResponse{ + TotalResult: res.Count, + AppliedLimit: uint64(request.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganizationMetadata(ctx context.Context, request *v2beta_org.DeleteOrganizationMetadataRequest) (*v2beta_org.DeleteOrganizationMetadataResponse, error) { + result, err := s.command.BulkRemoveOrgMetadata(ctx, request.OrganizationId, request.Keys...) + if err != nil { + return nil, err + } + return &v2beta_org.DeleteOrganizationMetadataResponse{ + DeletionDate: timestamppb.New(result.EventDate), + }, nil +} + +func (s *Server) DeactivateOrganization(ctx context.Context, request *org.DeactivateOrganizationRequest) (*org.DeactivateOrganizationResponse, error) { + objectDetails, err := s.command.DeactivateOrg(ctx, request.Id) + if err != nil { + return nil, err + } + return &org.DeactivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }, nil +} + +func (s *Server) ActivateOrganization(ctx context.Context, request *org.ActivateOrganizationRequest) (*org.ActivateOrganizationResponse, error) { + objectDetails, err := s.command.ReactivateOrg(ctx, request.Id) + if err != nil { + return nil, err + } + return &org.ActivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }, err +} + +func (s *Server) AddOrganizationDomain(ctx context.Context, request *org.AddOrganizationDomainRequest) (*org.AddOrganizationDomainResponse, error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Domain, request.OrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.AddOrgDomain(ctx, request.OrganizationId, request.Domain, userIDs) + if err != nil { + return nil, err + } + return &org.AddOrganizationDomainResponse{ + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) ListOrganizationDomains(ctx context.Context, req *org.ListOrganizationDomainsRequest) (*org.ListOrganizationDomainsResponse, error) { + queries, err := ListOrgDomainsRequestToModel(s.systemDefaults, req) + if err != nil { + return nil, err + } + orgIDQuery, err := query.NewOrgDomainOrgIDSearchQuery(req.OrganizationId) + if err != nil { + return nil, err + } + queries.Queries = append(queries.Queries, orgIDQuery) + + domains, err := s.query.SearchOrgDomains(ctx, queries, false) + if err != nil { + return nil, err + } + return &org.ListOrganizationDomainsResponse{ + Domains: object.DomainsToPb(domains.Domains), + Pagination: &filter.PaginationResponse{ + TotalResult: domains.Count, + AppliedLimit: uint64(req.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganizationDomain(ctx context.Context, req *org.DeleteOrganizationDomainRequest) (*org.DeleteOrganizationDomainResponse, error) { + details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &org.DeleteOrganizationDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, err +} + +func (s *Server) GenerateOrganizationDomainValidation(ctx context.Context, req *org.GenerateOrganizationDomainValidationRequest) (*org.GenerateOrganizationDomainValidationResponse, error) { + token, url, err := s.command.GenerateOrgDomainValidation(ctx, GenerateOrgDomainValidationRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &org.GenerateOrganizationDomainValidationResponse{ + Token: token, + Url: url, + }, nil +} + +func (s *Server) VerifyOrganizationDomain(ctx context.Context, request *org.VerifyOrganizationDomainRequest) (*org.VerifyOrganizationDomainResponse, error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Domain, request.OrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, request), userIDs) + if err != nil { + return nil, err + } + return &org.VerifyOrganizationDomainResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }, nil +} + +func createOrganizationRequestToCommand(request *v2beta_org.CreateOrganizationRequest) (*command.OrgSetup, error) { + admins, err := createOrganizationRequestAdminsToCommand(request.GetAdmins()) if err != nil { return nil, err } @@ -31,14 +212,14 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, - OrgID: request.GetOrgId(), + OrgID: request.GetId(), }, nil } -func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { +func createOrganizationRequestAdminsToCommand(requestAdmins []*v2beta_org.CreateOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { admins = make([]*command.OrgSetupAdmin, len(requestAdmins)) for i, admin := range requestAdmins { - admins[i], err = addOrganizationRequestAdminToCommand(admin) + admins[i], err = createOrganizationRequestAdminToCommand(admin) if err != nil { return nil, err } @@ -46,14 +227,14 @@ func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationR return admins, nil } -func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { +func createOrganizationRequestAdminToCommand(admin *v2beta_org.CreateOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { switch a := admin.GetUserType().(type) { - case *org.AddOrganizationRequest_Admin_UserId: + case *v2beta_org.CreateOrganizationRequest_Admin_UserId: return &command.OrgSetupAdmin{ ID: a.UserId, Roles: admin.GetRoles(), }, nil - case *org.AddOrganizationRequest_Admin_Human: + case *v2beta_org.CreateOrganizationRequest_Admin_Human: human, err := user.AddUserRequestToAddHuman(a.Human) if err != nil { return nil, err @@ -63,22 +244,31 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi Roles: admin.GetRoles(), }, nil default: - return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", a) + return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", a) } } -func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, - } +func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) { + queries := make([]query.SearchQuery, 0, 2) + loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase) + if err != nil { + return nil, err } - return &org.AddOrganizationResponse{ - Details: object.DomainToDetailsPb(createdOrg.ObjectDetails), - OrganizationId: createdOrg.ObjectDetails.ResourceOwner, - CreatedAdmins: admins, - }, nil + queries = append(queries, loginName) + if orgID != "" { + owner, err := query.NewUserResourceOwnerSearchQuery(orgID, query.TextNotEquals) + if err != nil { + return nil, err + } + queries = append(queries, owner) + } + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil) + if err != nil { + return nil, err + } + userIDs := make([]string, len(users.Users)) + for i, user := range users.Users { + userIDs[i] = user.ID + } + return userIDs, nil } diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 57ed05dfb2..2047f665a1 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -12,14 +12,13 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func Test_addOrganizationRequestToCommand(t *testing.T) { +func Test_createOrganizationRequestToCommand(t *testing.T) { type args struct { - request *org.AddOrganizationRequest + request *org.CreateOrganizationRequest } tests := []struct { name string @@ -30,21 +29,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "nil user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ {}, }, }, }, - wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), + wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", nil), }, { name: "custom org ID", args: args{ - request: &org.AddOrganizationRequest{ - Name: "custom org ID", - OrgId: gu.Ptr("org-ID"), + request: &org.CreateOrganizationRequest{ + Name: "custom org ID", + Id: gu.Ptr("org-ID"), }, }, want: &command.OrgSetup{ @@ -57,11 +56,11 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "user ID", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{ + UserType: &org.CreateOrganizationRequest_Admin_UserId{ UserId: "userID", }, Roles: nil, @@ -82,11 +81,11 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "human user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &org.CreateOrganizationRequest_Admin_Human{ Human: &user.AddHumanUserRequest{ Profile: &user.SetHumanProfile{ GivenName: "firstname", @@ -124,7 +123,7 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := addOrganizationRequestToCommand(tt.args.request) + got, err := createOrganizationRequestToCommand(tt.args.request) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) @@ -139,7 +138,7 @@ func Test_createdOrganizationToPb(t *testing.T) { tests := []struct { name string args args - want *org.AddOrganizationResponse + want *org.CreateOrganizationResponse wantErr error }{ { @@ -160,14 +159,10 @@ func Test_createdOrganizationToPb(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - Details: &object.Details{ - Sequence: 1, - ChangeDate: timestamppb.New(now), - ResourceOwner: "orgID", - }, - OrganizationId: "orgID", - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(now), + Id: "orgID", + CreatedAdmins: []*org.CreatedAdmin{ { UserId: "id", EmailCode: gu.Ptr("emailCode"), diff --git a/internal/api/grpc/org/v2beta/server.go b/internal/api/grpc/org/v2beta/server.go index 89dba81702..b7e8d4994f 100644 --- a/internal/api/grpc/org/v2beta/server.go +++ b/internal/api/grpc/org/v2beta/server.go @@ -6,6 +6,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" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" @@ -15,6 +16,7 @@ var _ org.OrganizationServiceServer = (*Server)(nil) type Server struct { org.UnimplementedOrganizationServiceServer + systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries checkPermission domain.PermissionCheck @@ -23,11 +25,13 @@ type Server struct { 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/query/org_metadata.go b/internal/query/org_metadata.go index 84b204de2b..fe61ad51d9 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -194,7 +194,6 @@ func prepareOrgMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, &m.Key, &m.Value, ) - if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, zerrors.ThrowNotFound(err, "QUERY-Rph32", "Errors.Metadata.NotFound") diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 1e7f3b7407..d8c88d540b 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -307,7 +307,6 @@ service AdminService { }; } - // Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/instance-service-list-custom-domains.api.mdx) instead to list custom domains rpc ListInstanceDomains(ListInstanceDomainsRequest) returns (ListInstanceDomainsResponse) { option (google.api.http) = { post: "/domains/_search"; @@ -320,12 +319,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Domains"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are the URLs where ZITADEL is running."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are the URLs where ZITADEL is running." }; } - // Deprecated: Use [ListTrustedDomains](apis/resources/instance_service_v2/instance-service-list-trusted-domains.api.mdx) instead to list trusted domains rpc ListInstanceTrustedDomains(ListInstanceTrustedDomainsRequest) returns (ListInstanceTrustedDomainsResponse) { option (google.api.http) = { post: "/trusted_domains/_search"; @@ -338,12 +335,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Trusted Domains"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } - // Deprecated: Use [AddTrustedDomain](apis/resources/instance_service_v2/instance-service-add-trusted-domain.api.mdx) instead to add a trusted domain rpc AddInstanceTrustedDomain(AddInstanceTrustedDomainRequest) returns (AddInstanceTrustedDomainResponse) { option (google.api.http) = { post: "/trusted_domains"; @@ -357,12 +352,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "Add an Instance Trusted Domain"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } - // Deprecated: Use [RemoveTrustedDomain](apis/resources/instance_service_v2/instance-service-remove-trusted-domain.api.mdx) instead to remove a trusted domain rpc RemoveInstanceTrustedDomain(RemoveInstanceTrustedDomainRequest) returns (RemoveInstanceTrustedDomainResponse) { option (google.api.http) = { delete: "/trusted_domains/{domain}"; @@ -375,8 +368,7 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "Remove an Instance Trusted Domain"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } @@ -1245,6 +1237,7 @@ service AdminService { }; } + // Deprecated: use ListOrganization [apis/resources/org_service_v2beta/organization-service-list-organizations.api.mdx] API instead rpc ListOrgs(ListOrgsRequest) returns (ListOrgsResponse) { option (google.api.http) = { post: "/orgs/_search"; @@ -1264,7 +1257,8 @@ service AdminService { value: { description: "list of organizations matching the query"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1279,6 +1273,7 @@ service AdminService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc SetUpOrg(SetUpOrgRequest) returns (SetUpOrgResponse) { option (google.api.http) = { post: "/orgs/_setup"; @@ -1298,7 +1293,8 @@ service AdminService { value: { description: "org, user and user membership were created successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1313,6 +1309,7 @@ service AdminService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/{org_id}" @@ -1330,7 +1327,8 @@ service AdminService { value: { description: "org removed successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 3018ebe600..34a8384d39 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2119,6 +2119,7 @@ service ManagementService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc AddOrg(AddOrgRequest) returns (AddOrgResponse) { option (google.api.http) = { post: "/orgs" @@ -2133,6 +2134,7 @@ service ManagementService { tags: "Organizations"; summary: "Create Organization"; description: "Create a new organization. Based on the given name a domain will be generated to be able to identify users within an organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2144,6 +2146,7 @@ service ManagementService { }; } + // Deprecated: use UpdateOrganization [apis/resources/org_service_v2beta/organization-service-update-organization.api.mdx] API instead rpc UpdateOrg(UpdateOrgRequest) returns (UpdateOrgResponse) { option (google.api.http) = { put: "/orgs/me" @@ -2158,6 +2161,7 @@ service ManagementService { tags: "Organizations"; summary: "Update Organization"; description: "Change the name of the organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2169,6 +2173,7 @@ service ManagementService { }; } + // Deprecated: use DeactivateOrganization [apis/resources/org_service_v2beta/organization-service-deactivate-organization.api.mdx] API instead rpc DeactivateOrg(DeactivateOrgRequest) returns (DeactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_deactivate" @@ -2183,6 +2188,7 @@ service ManagementService { tags: "Organizations"; summary: "Deactivate Organization"; description: "Sets the state of my organization to deactivated. Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2194,6 +2200,7 @@ service ManagementService { }; } + // Deprecated: use ActivateOrganization [apis/resources/org_service_v2beta/organization-service-activate-organization.api.mdx] API instead rpc ReactivateOrg(ReactivateOrgRequest) returns (ReactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_reactivate" @@ -2208,6 +2215,7 @@ service ManagementService { tags: "Organizations"; summary: "Reactivate Organization"; description: "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." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2219,6 +2227,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/me" @@ -2232,6 +2241,7 @@ service ManagementService { tags: "Organizations"; summary: "Delete Organization"; description: "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." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2243,6 +2253,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc SetOrgMetadata(SetOrgMetadataRequest) returns (SetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/{key}" @@ -2258,6 +2269,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Set Organization Metadata"; description: "This endpoint either adds or updates a metadata value for the requested key. Make sure the value is base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2269,6 +2281,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc BulkSetOrgMetadata(BulkSetOrgMetadataRequest) returns (BulkSetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_bulk" @@ -2284,6 +2297,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Bulk Set Organization Metadata"; description: "This endpoint sets a list of metadata to the organization. Make sure the values are base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2295,6 +2309,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc ListOrgMetadata(ListOrgMetadataRequest) returns (ListOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_search" @@ -2310,6 +2325,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Search Organization Metadata"; description: "Get the metadata of an organization filtered by your query." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2321,6 +2337,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc GetOrgMetadata(GetOrgMetadataRequest) returns (GetOrgMetadataResponse) { option (google.api.http) = { get: "/metadata/{key}" @@ -2335,6 +2352,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Get Organization Metadata By Key"; description: "Get a metadata object from an organization by a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2346,6 +2364,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc RemoveOrgMetadata(RemoveOrgMetadataRequest) returns (RemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/{key}" @@ -2360,6 +2379,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Delete Organization Metadata By Key"; description: "Remove a metadata object from an organization with a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2371,6 +2391,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc BulkRemoveOrgMetadata(BulkRemoveOrgMetadataRequest) returns (BulkRemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/_bulk" @@ -2384,6 +2405,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Organizations"; tags: "Organization Metadata"; + deprecated: true summary: "Bulk Delete Metadata"; description: "Remove a list of metadata objects from an organization with a list of keys." parameters: { @@ -2397,31 +2419,7 @@ service ManagementService { }; } - rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { - option (google.api.http) = { - post: "/orgs/me/domains/_search" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "org.read" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Organizations"; - summary: "Search Domains"; - description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - + // Deprecated: use AddOrganizationDomain [apis/resources/org_service_v2beta/organization-service-add-organization-domain.api.mdx] API instead rpc AddOrgDomain(AddOrgDomainRequest) returns (AddOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains" @@ -2436,6 +2434,7 @@ service ManagementService { tags: "Organizations"; summary: "Add Domain"; description: "Add a new domain to an organization. The domains are used to identify to which organization a user belongs." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2447,6 +2446,34 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationDomains [apis/resources/org_service_v2beta/organization-service-list-organization-domains.api.mdx] API instead + rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { + option (google.api.http) = { + post: "/orgs/me/domains/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.read" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Organizations"; + summary: "Search Domains"; + description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." + deprecated: true + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + + // Deprecated: use DeleteOrganizationDomain [apis/resources/org_service_v2beta/organization-service-delete-organization-domain.api.mdx] API instead rpc RemoveOrgDomain(RemoveOrgDomainRequest) returns (RemoveOrgDomainResponse) { option (google.api.http) = { delete: "/orgs/me/domains/{domain}" @@ -2460,6 +2487,7 @@ service ManagementService { tags: "Organizations"; summary: "Remove Domain"; description: "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." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2471,6 +2499,7 @@ service ManagementService { }; } + // Deprecated: use GenerateOrganizationDomainValidation [apis/resources/org_service_v2beta/organization-service-generate-organization-domain-validation.api.mdx] API instead rpc GenerateOrgDomainValidation(GenerateOrgDomainValidationRequest) returns (GenerateOrgDomainValidationResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_generate" @@ -2485,6 +2514,7 @@ service ManagementService { tags: "Organizations"; summary: "Generate Domain Verification"; description: "Generate a new file to be able to verify your domain with DNS or HTTP challenge." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2496,6 +2526,7 @@ service ManagementService { }; } + // Deprecated: use VerifyOrganizationDomain [apis/resources/org_service_v2beta/organization-service-verify-organization-domain.api.mdx] API instead rpc ValidateOrgDomain(ValidateOrgDomainRequest) returns (ValidateOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_validate" @@ -2510,6 +2541,7 @@ service ManagementService { tags: "Organizations"; summary: "Verify Domain"; description: "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." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2678,11 +2710,6 @@ service ManagementService { }; } - // Get Project By ID - // - // Deprecated: [Get Project](apis/resources/project_service_v2/project-service-get-project.api.mdx) to get project by ID. - // - // Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc GetProjectByID(GetProjectByIDRequest) returns (GetProjectByIDResponse) { option (google.api.http) = { get: "/projects/{id}" @@ -2695,7 +2722,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Get Project By ID"; + description: "Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2707,11 +2735,6 @@ service ManagementService { }; } - // Get Granted Project By ID - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to get granted projects. - // - // Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context. rpc GetGrantedProjectByID(GetGrantedProjectByIDRequest) returns (GetGrantedProjectByIDResponse) { option (google.api.http) = { get: "/granted_projects/{project_id}/grants/{grant_id}" @@ -2724,7 +2747,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Get Granted Project By ID"; + description: "Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2736,11 +2760,6 @@ service ManagementService { }; } - // List Projects - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to list all projects and granted projects. - // - // Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) { option (google.api.http) = { post: "/projects/_search" @@ -2753,7 +2772,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Search Project"; + description: "Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2765,11 +2785,6 @@ service ManagementService { }; } - // List Granted Projects - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to list all projects and granted projects. - // - // Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context. rpc ListGrantedProjects(ListGrantedProjectsRequest) returns (ListGrantedProjectsResponse) { option (google.api.http) = { post: "/granted_projects/_search" @@ -2782,7 +2797,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Search Granted Project"; + description: "Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2844,11 +2860,6 @@ service ManagementService { }; } - // Create Project - // - // Deprecated: [Create Project](apis/resources/project_service_v2/project-service-create-project.api.mdx) to create a project. - // - // Create a new project. A Project is a vessel for different applications sharing the same role context. rpc AddProject(AddProjectRequest) returns (AddProjectResponse) { option (google.api.http) = { post: "/projects" @@ -2861,7 +2872,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Create Project"; + description: "Create a new project. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2873,11 +2885,6 @@ service ManagementService { }; } - // Update Project - // - // Deprecated: [Update Project](apis/resources/project_service_v2/project-service-update-project.api.mdx) to update a project. - // - // Update a project and its settings. A Project is a vessel for different applications sharing the same role context. rpc UpdateProject(UpdateProjectRequest) returns (UpdateProjectResponse) { option (google.api.http) = { put: "/projects/{id}" @@ -2891,7 +2898,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Update Project"; + description: "Update a project and its settings. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2903,11 +2911,6 @@ service ManagementService { }; } - // Deactivate Project - // - // Deprecated: [Deactivate Project](apis/resources/project_service_v2/project-service-deactivate-project.api.mdx) to deactivate a project. - // - // Set the state of a project to deactivated. Request returns an error if the project is already deactivated. rpc DeactivateProject(DeactivateProjectRequest) returns (DeactivateProjectResponse) { option (google.api.http) = { post: "/projects/{id}/_deactivate" @@ -2921,7 +2924,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Deactivate Project"; + description: "Set the state of a project to deactivated. Request returns an error if the project is already deactivated." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2933,11 +2937,6 @@ service ManagementService { }; } - // Activate Project - // - // Deprecated: [Activate Project](apis/resources/project_service_v2/project-service-activate-project.api.mdx) to activate a project. - // - // Set the state of a project to active. Request returns an error if the project is not deactivated. rpc ReactivateProject(ReactivateProjectRequest) returns (ReactivateProjectResponse) { option (google.api.http) = { post: "/projects/{id}/_reactivate" @@ -2951,7 +2950,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Reactivate Project"; + description: "Set the state of a project to active. Request returns an error if the project is not deactivated." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2963,11 +2963,6 @@ service ManagementService { }; } - // Remove Project - // - // Deprecated: [Delete Project](apis/resources/project_service_v2/project-service-delete-project.api.mdx) to remove a project. - // - // Project and all its sub-resources like project grants, applications, roles and user grants will be removed. rpc RemoveProject(RemoveProjectRequest) returns (RemoveProjectResponse) { option (google.api.http) = { delete: "/projects/{id}" @@ -2980,7 +2975,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Remove Project"; + description: "Project and all its sub-resources like project grants, applications, roles and user grants will be removed." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2992,11 +2988,6 @@ service ManagementService { }; } - // Search Project Roles - // - // Deprecated: [List Project Roles](apis/resources/project_service_v2/project-service-list-project-roles.api.mdx) to get project roles. - // - // Returns all roles of a project matching the search query. rpc ListProjectRoles(ListProjectRolesRequest) returns (ListProjectRolesResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles/_search" @@ -3010,7 +3001,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Search Project Roles"; + description: "Returns all roles of a project matching the search query." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3022,11 +3014,6 @@ service ManagementService { }; } - // Add Project Role - // - // Deprecated: [Add Project Role](apis/resources/project_service_v2/project-service-add-project-role.api.mdx) to add a project role. - // - // Add a new project role to a project. The key must be unique within the project.\n\nDeprecated: please use user service v2 AddProjectRole. rpc AddProjectRole(AddProjectRoleRequest) returns (AddProjectRoleResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles" @@ -3040,7 +3027,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Add Project Role"; + description: "Add a new project role to a project. The key must be unique within the project." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3052,11 +3040,6 @@ service ManagementService { }; } - // Bulk add Project Role - // - // Deprecated: [Add Project Role](apis/resources/project_service_v2/project-service-add-project-role.api.mdx) to add a project role. - // - // Add a list of roles to a project. The keys must be unique within the project. rpc BulkAddProjectRoles(BulkAddProjectRolesRequest) returns (BulkAddProjectRolesResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles/_bulk" @@ -3070,7 +3053,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Bulk Add Project Role"; + description: "Add a list of roles to a project. The keys must be unique within the project." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3082,11 +3066,6 @@ service ManagementService { }; } - // Update Project Role - // - // Deprecated: [Update Project Role](apis/resources/project_service_v2/project-service-update-project-role.api.mdx) to update a project role. - // - // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. rpc UpdateProjectRole(UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { option (google.api.http) = { put: "/projects/{project_id}/roles/{role_key}" @@ -3100,7 +3079,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Change Project Role"; + description: "Change a project role. The key is not editable. If a key should change, remove the role and create a new one." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3112,11 +3092,6 @@ service ManagementService { }; } - // Remove Project Role - // - // Deprecated: [Delete Project Role](apis/resources/project_service_v2/project-service-update-project-role.api.mdx) to remove a project role. - // - // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. rpc RemoveProjectRole(RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { option (google.api.http) = { delete: "/projects/{project_id}/roles/{role_key}" @@ -3129,7 +3104,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Remove Project Role"; + description: "Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3793,11 +3769,6 @@ service ManagementService { }; } - // Get Project Grant By ID - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to get a project grant. - // - // Returns a project grant. A project grant is when the organization grants its project to another organization. rpc GetProjectGrantByID(GetProjectGrantByIDRequest) returns (GetProjectGrantByIDResponse) { option (google.api.http) = { get: "/projects/{project_id}/grants/{grant_id}" @@ -3809,7 +3780,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Project Grant By ID"; + description: "Returns a project grant. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3821,11 +3793,6 @@ service ManagementService { }; } - // List Project Grants - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to list project grants. - // - // Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization. rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/_search" @@ -3839,7 +3806,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Search Project Grants from Project"; + description: "Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3851,11 +3819,6 @@ service ManagementService { }; } - // Search Project Grants - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to list project grants. - // - // Returns a list of project grants. A project grant is when the organization grants its project to another organization. rpc ListAllProjectGrants(ListAllProjectGrantsRequest) returns (ListAllProjectGrantsResponse) { option (google.api.http) = { post: "/projectgrants/_search" @@ -3868,7 +3831,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Search Project Grants"; + description: "Returns a list of project grants. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3880,11 +3844,6 @@ service ManagementService { }; } - // Add Project Grant - // - // Deprecated: [Create Project Grant](apis/resources/project_service_v2/project-service-create-project-grant.api.mdx) to add a project grant. - // - // Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc AddProjectGrant(AddProjectGrantRequest) returns (AddProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants" @@ -3897,7 +3856,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Add Project Grant"; + description: "Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization" parameters: { headers: { name: "x-zitadel-orgid"; @@ -3909,11 +3869,6 @@ service ManagementService { }; } - // Update Project Grant - // - // Deprecated: [Update Project Grant](apis/resources/project_service_v2/project-service-update-project-grant.api.mdx) to update a project grant. - // - // Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc UpdateProjectGrant(UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { option (google.api.http) = { put: "/projects/{project_id}/grants/{grant_id}" @@ -3926,7 +3881,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Change Project Grant"; + description: "Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization" parameters: { headers: { name: "x-zitadel-orgid"; @@ -3938,11 +3894,6 @@ service ManagementService { }; } - // Deactivate Project Grant - // - // Deprecated: [Deactivate Project Grant](apis/resources/project_service_v2/project-service-deactivate-project-grant.api.mdx) to deactivate a project grant. - // - // Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate. rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/{grant_id}/_deactivate" @@ -3955,7 +3906,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Deactivate Project Grant"; + description: "Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3967,11 +3919,6 @@ service ManagementService { }; } - // Reactivate Project Grant - // - // Deprecated: [Activate Project Grant](apis/resources/project_service_v2/project-service-activate-project-grant.api.mdx) to activate a project grant. - // - // Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate. rpc ReactivateProjectGrant(ReactivateProjectGrantRequest) returns (ReactivateProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/{grant_id}/_reactivate" @@ -3984,7 +3931,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Reactivate Project Grant"; + description: "Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3996,11 +3944,6 @@ service ManagementService { }; } - // Remove Project Grant - // - // Deprecated: [Delete Project Grant](apis/resources/project_service_v2/project-service-delete-project-grant.api.mdx) to remove a project grant. - // - // Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked). rpc RemoveProjectGrant(RemoveProjectGrantRequest) returns (RemoveProjectGrantResponse) { option (google.api.http) = { delete: "/projects/{project_id}/grants/{grant_id}" @@ -4012,7 +3955,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Remove Project Grant"; + description: "Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked)." parameters: { headers: { name: "x-zitadel-orgid"; diff --git a/proto/zitadel/metadata/v2beta/metadata.proto b/proto/zitadel/metadata/v2beta/metadata.proto new file mode 100644 index 0000000000..87fcc51869 --- /dev/null +++ b/proto/zitadel/metadata/v2beta/metadata.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +import "zitadel/object/v2beta/object.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.metadata.v2beta; + +option go_package ="github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta"; + +message Metadata { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata key", + example: "\"key1\""; + } + ]; + bytes value = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata value is base64 encoded, make sure to decode to get the value", + example: "\"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\""; + } + ]; +} + +message MetadataQuery { + oneof query { + option (validate.required) = true; + MetadataKeyQuery key_query = 1; + } +} + +message MetadataKeyQuery { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"key\"" + } + ]; + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} diff --git a/proto/zitadel/org/v2beta/org.proto b/proto/zitadel/org/v2beta/org.proto new file mode 100644 index 0000000000..08cf47e820 --- /dev/null +++ b/proto/zitadel/org/v2beta/org.proto @@ -0,0 +1,169 @@ +syntax = "proto3"; + +package zitadel.org.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/object/v2beta/object.proto"; +import "google/protobuf/timestamp.proto"; + +message Organization { + // Unique identifier of the organization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The timestamp of the verification of the organization domain. + google.protobuf.Timestamp changed_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + // Current state of the organization, for example active, inactive and deleted. + OrgState state = 4; + + // Name of the organization. + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Primary domain used in the organization. + string primary_domain = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; +} + +enum OrgState { + ORG_STATE_UNSPECIFIED = 0; + ORG_STATE_ACTIVE = 1; + ORG_STATE_INACTIVE = 2; + ORG_STATE_REMOVED = 3; +} + +enum OrgFieldName { + ORG_FIELD_NAME_UNSPECIFIED = 0; + ORG_FIELD_NAME_NAME = 1; + ORG_FIELD_NAME_CREATION_DATE = 2; +} + +message OrganizationSearchFilter{ + oneof filter { + option (validate.required) = true; + + OrgNameFilter name_filter = 1; + OrgDomainFilter domain_filter = 2; + OrgStateFilter state_filter = 3; + OrgIDFilter id_filter = 4; + } +} +message OrgNameFilter { + // Organization name. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgDomainFilter { + // The domain. + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgStateFilter { + // Current state of the organization. + OrgState state = 1 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgIDFilter { + // The Organization id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +// from proto/zitadel/org.proto +message DomainSearchFilter { + oneof filter { + option (validate.required) = true; + DomainNameFilter domain_name_filter = 1; + } +} + +// from proto/zitadel/org.proto +message DomainNameFilter { + // The domain. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +// from proto/zitadel/org.proto +message Domain { + // The Organization id. + string organization_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The domain name. + string domain_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\""; + } + ]; + // Defines if the domain is verified. + bool is_verified = 3; + // Defines if the domain is the primary domain. + bool is_primary = 4; + // Defines the protocol the domain was validated with. + DomainValidationType validation_type = 5; +} + +// from proto/zitadel/org.proto +enum DomainValidationType { + DOMAIN_VALIDATION_TYPE_UNSPECIFIED = 0; + DOMAIN_VALIDATION_TYPE_HTTP = 1; + DOMAIN_VALIDATION_TYPE_DNS = 2; +} diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index e303b676d7..28c823a89b 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -6,24 +6,22 @@ package zitadel.org.v2beta; import "zitadel/object/v2beta/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2beta/auth.proto"; -import "zitadel/user/v2beta/email.proto"; -import "zitadel/user/v2beta/phone.proto"; -import "zitadel/user/v2beta/idp.proto"; -import "zitadel/user/v2beta/password.proto"; -import "zitadel/user/v2beta/user.proto"; +import "zitadel/org/v2beta/org.proto"; +import "zitadel/metadata/v2beta/metadata.proto"; import "zitadel/user/v2beta/user_service.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { - title: "User Service"; + title: "Organization Service (Beta)"; version: "2.0-beta"; description: "This API is intended to manage organizations in a ZITADEL instance. This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login."; contact:{ @@ -111,8 +109,13 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service OrganizationService { - // Create a new organization and grant the user(s) permission to manage it - rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) { + // Create 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 CreateOrganization(CreateOrganizationRequest) returns (CreateOrganizationResponse) { option (google.api.http) = { post: "/v2beta/organizations" body: "*" @@ -122,34 +125,411 @@ service OrganizationService { auth_option: { permission: "org.create" } - http_response: { - success_code: 201 - } }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create an Organization"; - description: "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." + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { responses: { - key: "200" + key: "200"; value: { - description: "OK"; + description: "Organization created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The organization to create already exists."; } }; }; } + + // Update Organization + // + // Change the name of the organization. + // + // Required permission: + // - `org.write` + rpc UpdateOrganization(UpdateOrganizationRequest) returns (UpdateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + 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"; + } + }; + responses: { + key: "409" + value: { + description: "Organisation's name already taken"; + } + }; + }; + + } + + // List Organizations + // + // Returns a list of organizations that match the requesting filters. All filters are applied with an AND condition. + // + // Required permission: + // - `iam.read` + rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/search"; + body: "*"; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // 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: "/v2beta/organizations/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.delete"; + } + }; + + 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: "/v2beta/organizations/{organization_id}/metadata" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + // TODO This needs to chagne to 404 + 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: "/v2beta/organizations/{organization_id}/metadata/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { + permission: "org.read" + } + }; + + 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: "/v2beta/organizations/{organization_id}/metadata" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + 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: "/v2beta/organizations/{organization_id}/domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + 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: "/v2beta/organizations/{organization_id}/domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.read" + } + }; + + 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: "/v2beta/organizations/{organization_id}/domains" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + 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: "/v2beta/organizations/{organization_id}/domains/validation/generate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + 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: "/v2beta/organizations/{organization_id}/domains/validation/verify" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + 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: "/v2beta/organizations/{id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + 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: "/v2beta/organizations/{id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + } -message AddOrganizationRequest{ +message CreateOrganizationRequest{ + // The Admin for the newly created Organization. message Admin { oneof user_type{ string user_id = 1; zitadel.user.v2beta.AddHumanUserRequest human = 2; } - // specify Org Member Roles for the provided user (default is ORG_OWNER if roles are empty) + // specify Organization Member Roles for the provided user (default is ORG_OWNER if roles are empty) repeated string roles = 3; } + // name of the Organization to be created. string name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, @@ -159,24 +539,403 @@ message AddOrganizationRequest{ example: "\"ZITADEL\""; } ]; - repeated Admin admins = 2; - // optionally set your own id unique for the organization. - optional string org_id = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + // Optionally set your own id unique for the organization. + optional string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200 }, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; max_length: 200; - example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + example: "\"69629012906488334\""; + } + ]; + // Additional Admins for the Organization. + repeated Admin admins = 3; +} + +message CreatedAdmin { + string user_id = 1; + optional string email_code = 2; + optional string phone_code = 3; +} + +message CreateOrganizationResponse{ + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // Organization ID of the newly created organization. + string id = 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: "\"69629012906488334\""; + } + ]; + + // The admins created for the Organization + repeated CreatedAdmin created_admins = 3; +} + +message UpdateOrganizationRequest { + // Organization Id for the Organization to be updated + string 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\""; + } + ]; + + // New Name for the Organization to be updated + 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 AddOrganizationResponse{ - message CreatedAdmin { - string user_id = 1; - optional string email_code = 2; - optional string phone_code = 3; - } - zitadel.object.v2beta.Details details = 1; - string organization_id = 2; - repeated CreatedAdmin created_admins = 3; +message UpdateOrganizationResponse { + // 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. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // the field the result is sorted + zitadel.org.v2beta.OrgFieldName sorting_column = 2; + // Define the criteria to query for. + // repeated ProjectRoleQuery filters = 4; + repeated zitadel.org.v2beta.OrganizationSearchFilter filter = 3; +} + +message ListOrganizationsResponse { + // Pagination of the Organizations results + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organizations requested + repeated zitadel.org.v2beta.Organization organizations = 2; +} + +message DeleteOrganizationRequest { + + // Organization Id for the Organization to be deleted + string 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 { + // 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 { + // Organization Id for the Organization to be deactivated + string 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; + } + ]; +} + +message DeactivateOrganizationResponse { + // 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 { + // Organization Id for the Organization to be activated + string 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; + } + ]; +} + +message ActivateOrganizationResponse { + // 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 { + // Organization Id for the Organization for which the domain is to be added to. + 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\""; + } + ]; + // The domain you want to add to the organization. + 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 { + // The timestamp of the organization 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 { + // Organization Id for the Organization which 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.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated DomainSearchFilter filters = 3; +} + +message ListOrganizationDomainsResponse { + // Pagination of the Organizations domain results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The domains requested. + repeated Domain domains = 2; +} + +message DeleteOrganizationDomainRequest { + // Organization Id for the Organization which 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\""; + } + ]; + 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 { + // 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 { + // Organization Id for the Organization which doman to be validated. + 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\""; + } + ]; + // The domain which to be deleted. + 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\""; + } + ]; + DomainValidationType type = 3 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message GenerateOrganizationDomainValidationResponse { + // The token verify domain. + string token = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; + // URL used to verify the domain. + string url = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://testdomain.com/.well-known/zitadel-challenge/ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; +} + +message VerifyOrganizationDomainRequest { + // Organization Id for the Organization doman 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\""; + } + ]; + // Organization Id for the Organization doman to be verified. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message VerifyOrganizationDomainResponse { + // 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 in the metadata key/value pair. + string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // Value in the metadata key/value pair. + bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}]; +} +message SetOrganizationMetadataRequest{ + // Organization Id for the Organization doman 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\""; + } + ]; + // Metadata to set. + repeated Metadata metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title: "Medata (Key/Value)" + description: "The values have to be base64 encoded."; + 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 of Orgalization which 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.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated zitadel.metadata.v2beta.MetadataQuery filter = 3; +} + +message ListOrganizationMetadataResponse { + // Pagination of the Organizations metadata results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organization metadata requested. + repeated zitadel.metadata.v2beta.Metadata metadata = 2; +} + +message DeleteOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be deleted is stored on. + 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\""; + } + ]; + // The keys for the Organization metadata to be deleted. + repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}]; +} + +message DeleteOrganizationMetadataResponse{ + // The timestamp of the deletiion 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\""; + } + ]; }