From 0f2a349ec145fc3efe0ec45db52d1f974fa180d2 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 17 Oct 2025 07:35:35 +0200 Subject: [PATCH] feat(api): move internal permission service to GA (and deprecate v2beta) (#10898) # Which Problems Are Solved As part of our efforts to simplify the structure and versions of our APIs, were moving all existing v2beta endpoints to v2 and deprecate them. They will be removed in Zitadel V5. # How the Problems Are Solved - This PR moves the internal permission v2beta service and its endpoints to a corresponding v2 version. The v2beta service and endpoints are deprecated. - The docs are moved to the new GA service and its endpoints. The v2beta is not displayed anymore. - The comments and have been improved and, where not already done, moved from swagger annotations to proto. - All required fields have been marked with (google.api.field_behavior) = REQUIRED and validation rules have been added where missing. - Listing administrators of a project grant can now be done with the `ProjectGrant` (`project_id` and `organization_id`) instead of a `project_id`, which corresponds to creation of the administrator ship of such grant. - formatted using `buf` # Additional Changes None # Additional Context - part of https://github.com/zitadel/zitadel/issues/10772 - requires backport to v4.x --------- Co-authored-by: Gayathri Vijayan <66356931+grvijayan@users.noreply.github.com> --- cmd/start/start.go | 4 + docs/docusaurus.config.js | 2 +- docs/sidebars.js | 8 +- .../internal_permission/v2/administrator.go | 220 ++ .../v2/integration_test/administrator_test.go | 1898 +++++++++++++++++ .../v2/integration_test/query_test.go | 1320 ++++++++++++ .../v2/integration_test/server_test.go | 65 + .../api/grpc/internal_permission/v2/query.go | 208 ++ .../api/grpc/internal_permission/v2/server.go | 59 + internal/integration/client.go | 3 + internal/query/administrators.go | 1 - internal/query/user_membership.go | 4 + pkg/grpc/internal_permission/v2/resource.go | 3 + proto/zitadel/admin.proto | 8 +- .../v2/internal_permission_service.proto | 236 ++ .../internal_permission/v2/query.proto | 234 ++ .../v2beta/internal_permission_service.proto | 14 + proto/zitadel/management.proto | 26 +- 18 files changed, 4289 insertions(+), 24 deletions(-) create mode 100644 internal/api/grpc/internal_permission/v2/administrator.go create mode 100644 internal/api/grpc/internal_permission/v2/integration_test/administrator_test.go create mode 100644 internal/api/grpc/internal_permission/v2/integration_test/query_test.go create mode 100644 internal/api/grpc/internal_permission/v2/integration_test/server_test.go create mode 100644 internal/api/grpc/internal_permission/v2/query.go create mode 100644 internal/api/grpc/internal_permission/v2/server.go create mode 100644 pkg/grpc/internal_permission/v2/resource.go create mode 100644 proto/zitadel/internal_permission/v2/internal_permission_service.proto create mode 100644 proto/zitadel/internal_permission/v2/query.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index 1378779c1d7..ec72a74e20f 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -46,6 +46,7 @@ import ( group_v2 "github.com/zitadel/zitadel/internal/api/grpc/group/v2" idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2" instance "github.com/zitadel/zitadel/internal/api/grpc/instance/v2beta" + internal_permission_v2 "github.com/zitadel/zitadel/internal/api/grpc/internal_permission/v2" internal_permission_v2beta "github.com/zitadel/zitadel/internal/api/grpc/internal_permission/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/management" oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2" @@ -536,6 +537,9 @@ func startAPIs( if err := apis.RegisterService(ctx, internal_permission_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, internal_permission_v2.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { return nil, err } diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index f8c08173ce9..3cb69e886b1 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -428,7 +428,7 @@ module.exports = { }, internal_permission_v2: { specPath: - ".artifacts/openapi3/zitadel/internal_permission/v2beta/internal_permission_service.openapi.yaml", + ".artifacts/openapi3/zitadel/internal_permission/v2/internal_permission_service.openapi.yaml", outputDir: "docs/apis/resources/internal_permission_service_v2", sidebarOptions: { groupPathsBy: "tag", diff --git a/docs/sidebars.js b/docs/sidebars.js index 08a3fcf2fec..f71f99c9612 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -907,15 +907,13 @@ module.exports = { }, { type: "category", - label: "Internal Permissions (Beta)", + label: "Internal Permissions", link: { type: "generated-index", - title: "Internal Permission Service API (Beta)", + title: "Internal Permission Service API", slug: "/apis/resources/internal_permission_service_v2", description: - "This API is intended to manage internal permissions in ZITADEL.\n" + - "\n" + - "This API is in beta state. It can AND will continue breaking until a stable version is released.\n" + "This API provides methods to manage permissions for resource and and their management in ZITADEL itself also known as \"administrators\"." }, items: sidebar_api_internal_permission_service_v2, }, diff --git a/internal/api/grpc/internal_permission/v2/administrator.go b/internal/api/grpc/internal_permission/v2/administrator.go new file mode 100644 index 00000000000..a9f526a0ec4 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2/administrator.go @@ -0,0 +1,220 @@ +package internal_permission + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2" +) + +func (s *Server) CreateAdministrator(ctx context.Context, req *connect.Request[internal_permission.CreateAdministratorRequest]) (*connect.Response[internal_permission.CreateAdministratorResponse], error) { + var creationDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.AddInstanceMember(ctx, createAdministratorInstanceToCommand(authz.GetInstance(ctx).InstanceID(), req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.AddOrgMember(ctx, createAdministratorOrganizationToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.AddProjectMember(ctx, createAdministratorProjectToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.AddProjectGrantMember(ctx, createAdministratorProjectGrantToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-IbPp47HDP5", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.CreateAdministratorResponse{ + CreationDate: creationDate, + }), nil +} + +func createAdministratorInstanceToCommand(instanceID, userID string, roles []string) *command.AddInstanceMember { + return &command.AddInstanceMember{ + InstanceID: instanceID, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorOrganizationToCommand(req *internal_permission.ResourceType_OrganizationId, userID string, roles []string) *command.AddOrgMember { + return &command.AddOrgMember{ + OrgID: req.OrganizationId, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorProjectToCommand(req *internal_permission.ResourceType_ProjectId, userID string, roles []string) *command.AddProjectMember { + return &command.AddProjectMember{ + ProjectID: req.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.AddProjectGrantMember { + return &command.AddProjectGrantMember{ + OrganizationID: req.ProjectGrant.OrganizationId, + ProjectID: req.ProjectGrant.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func (s *Server) UpdateAdministrator(ctx context.Context, req *connect.Request[internal_permission.UpdateAdministratorRequest]) (*connect.Response[internal_permission.UpdateAdministratorResponse], error) { + var changeDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.ChangeInstanceMember(ctx, updateAdministratorInstanceToCommand(authz.GetInstance(ctx).InstanceID(), req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.ChangeOrgMember(ctx, updateAdministratorOrganizationToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.ChangeProjectMember(ctx, updateAdministratorProjectToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.ChangeProjectGrantMember(ctx, updateAdministratorProjectGrantToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-i0V2IbdloZ", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.UpdateAdministratorResponse{ + ChangeDate: changeDate, + }), nil +} + +func updateAdministratorInstanceToCommand(instanceID, userID string, roles []string) *command.ChangeInstanceMember { + return &command.ChangeInstanceMember{ + InstanceID: instanceID, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorOrganizationToCommand(req *internal_permission.ResourceType_OrganizationId, userID string, roles []string) *command.ChangeOrgMember { + return &command.ChangeOrgMember{ + OrgID: req.OrganizationId, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorProjectToCommand(req *internal_permission.ResourceType_ProjectId, userID string, roles []string) *command.ChangeProjectMember { + return &command.ChangeProjectMember{ + ProjectID: req.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.ChangeProjectGrantMember { + return &command.ChangeProjectGrantMember{ + OrganizationID: req.ProjectGrant.OrganizationId, + ProjectID: req.ProjectGrant.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func (s *Server) DeleteAdministrator(ctx context.Context, req *connect.Request[internal_permission.DeleteAdministratorRequest]) (*connect.Response[internal_permission.DeleteAdministratorResponse], error) { + var deletionDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.RemoveInstanceMember(ctx, authz.GetInstance(ctx).InstanceID(), req.Msg.UserId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.RemoveOrgMember(ctx, resource.OrganizationId, req.Msg.UserId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.RemoveProjectMember(ctx, resource.ProjectId, req.Msg.UserId, "") + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.RemoveProjectGrantMember(ctx, resource.ProjectGrant.ProjectId, req.Msg.UserId, "", resource.ProjectGrant.OrganizationId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-3UOjLtuohh", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.DeleteAdministratorResponse{ + DeletionDate: deletionDate, + }), nil +} diff --git a/internal/api/grpc/internal_permission/v2/integration_test/administrator_test.go b/internal/api/grpc/internal_permission/v2/integration_test/administrator_test.go new file mode 100644 index 00000000000..673d9d413d0 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2/integration_test/administrator_test.go @@ -0,0 +1,1898 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2" +) + +func TestServer_CreateAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.CreateAdministratorRequest) + req *internal_permission.CreateAdministratorRequest + want + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "empty roles", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "notexisting", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{}, + }, + wantErr: true, + }, + { + name: "empty resource", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "notexisting", + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "instance, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, no existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: "notexisting", + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionV2.CreateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_CreateAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + grantedProjectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + userGrantedProjectResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectMembership(t, iamOwnerCtx, grantedProjectResp.GetId(), userGrantedProjectResp.GetUserId()) + patGrantedProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userGrantedProjectResp.GetUserId()) + grantedProjectOwnerCtx := integration.WithAuthorizationToken(CTX, patGrantedProjectResp.Token) + instance.CreateProjectGrant(iamOwnerCtx, t, grantedProjectResp.GetId(), instance.DefaultOrg.GetId()) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.CreateAdministratorRequest) + req *internal_permission.CreateAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project grant, org owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: grantedProjectResp.GetId(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, project owner, error", + ctx: grantedProjectOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: grantedProjectResp.GetId(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionV2.CreateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertCreateAdministratorResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *internal_permission.CreateAdministratorResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.UpdateAdministratorRequest) + req *internal_permission.UpdateAdministratorRequest + want + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "empty roles", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "notexisting", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{}, + }, + wantErr: true, + }, + { + name: "empty resource", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "notexisting", + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "org, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "org, no existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "project, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "project, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: "notexisting", + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "project grant, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "project grant, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.req) + } + got, err := instance.Client.InternalPermissionV2.UpdateAdministrator(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateAdministratorResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.UpdateAdministratorRequest) + req *internal_permission.UpdateAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, project owner, error", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionV2.UpdateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertUpdateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertUpdateAdministratorResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *internal_permission.UpdateAdministratorResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) + req *internal_permission.DeleteAdministratorRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.DeleteAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "instance, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + instance.DeleteInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "org, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + instance.DeleteOrgMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + instance.DeleteProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: "notexisting", + }, + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: false, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project grant, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + instance.DeleteProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + } + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.InternalPermissionV2.DeleteAdministrator(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteAdministratorResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.DeleteAdministratorRequest) + req *internal_permission.DeleteAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "project grant, project owner, error", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionV2.DeleteAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertDeleteAdministratorResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *internal_permission.DeleteAdministratorResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} diff --git a/internal/api/grpc/internal_permission/v2/integration_test/query_test.go b/internal/api/grpc/internal_permission/v2/integration_test/query_test.go new file mode 100644 index 00000000000..522979f84e0 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2/integration_test/query_test.go @@ -0,0 +1,1320 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2" +) + +func TestServer_ListAdministrators(t *testing.T) { + // instance administrators with IAM_OWNER role for tests and 3 instance administrators to query + iamOwnerCtx := instanceQuery.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + instanceAdministrator1 := createInstanceAdministrator(iamOwnerCtx, instanceQuery, t) + instanceAdministrator2 := createInstanceAdministrator(iamOwnerCtx, instanceQuery, t) + instanceAdministrator3 := createInstanceAdministrator(iamOwnerCtx, instanceQuery, t) + + // organization setup with 3 organization administrators to query + organizationName := integration.OrganizationName() + organizationResp := instanceQuery.CreateOrganization(iamOwnerCtx, organizationName, integration.Email()) + organizationAdmin1 := createOrganizationAdministrator(iamOwnerCtx, instanceQuery, organizationResp.GetOrganizationId(), organizationName, t) + organizationAdmin2 := createOrganizationAdministrator(iamOwnerCtx, instanceQuery, organizationResp.GetOrganizationId(), organizationName, t) + organizationAdmin3 := createOrganizationAdministrator(iamOwnerCtx, instanceQuery, organizationResp.GetOrganizationId(), organizationName, t) + + // Additionally, a machine user as a project grant owner to test permissions + userOrganizationResp := instanceQuery.CreateMachineUser(iamOwnerCtx) + instanceQuery.CreateOrgMembership(t, iamOwnerCtx, organizationResp.GetOrganizationId(), userOrganizationResp.GetUserId()) + patOrganizationResp := instanceQuery.CreatePersonalAccessToken(iamOwnerCtx, userOrganizationResp.GetUserId()) + organizationOwnerCtx := integration.WithAuthorizationToken(CTX, patOrganizationResp.Token) + + // project setup on the same organization with 3 project administrators to query + projectName := integration.ProjectName() + projectResp := instanceQuery.CreateProject(iamOwnerCtx, t, organizationResp.GetOrganizationId(), projectName, false, false) + projectAdministrator1 := createProjectAdministrator(iamOwnerCtx, instanceQuery, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName) + projectAdministrator2 := createProjectAdministrator(iamOwnerCtx, instanceQuery, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName) + projectAdministrator3 := createProjectAdministrator(iamOwnerCtx, instanceQuery, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName) + + // Additionally, a machine user as a project owner to test permissions + userProjectResp := instanceQuery.CreateMachineUser(iamOwnerCtx) + instanceQuery.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instanceQuery.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + // project grant setup on the same project with 3 project grant administrators to query + grantedOrganizationName := integration.OrganizationName() + grantedOrganizationResp := instanceQuery.CreateOrganization(iamOwnerCtx, grantedOrganizationName, integration.Email()) + instanceQuery.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrganizationResp.GetOrganizationId()) + projectGrantAdministrator1 := createProjectGrantAdministrator(iamOwnerCtx, instanceQuery, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName, grantedOrganizationResp.GetOrganizationId()) + projectGrantAdministrator2 := createProjectGrantAdministrator(iamOwnerCtx, instanceQuery, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName, grantedOrganizationResp.GetOrganizationId()) + projectGrantAdministrator3 := createProjectGrantAdministrator(iamOwnerCtx, instanceQuery, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName, grantedOrganizationResp.GetOrganizationId()) + + // Additionally, a machine user as a project grant owner to test permissions + userProjectGrantResp := instanceQuery.CreateMachineUser(iamOwnerCtx) + instanceQuery.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedOrganizationResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instanceQuery.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type args struct { + ctx context.Context + dep func(*internal_permission.ListAdministratorsRequest, *internal_permission.ListAdministratorsResponse) + req *internal_permission.ListAdministratorsRequest + } + tests := []struct { + name string + args args + want *internal_permission.ListAdministratorsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instanceQuery.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: organizationOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{ + { + Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{Ids: []string{"notexisting"}}, + }, + }, + }, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = instanceAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), instanceAdministrator2.GetUser().GetId(), instanceAdministrator3.GetUser().GetId()}, + }, + } + response.Administrators[0] = instanceAdministrator3 + response.Administrators[1] = instanceAdministrator2 + response.Administrators[2] = instanceAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{organizationAdmin1.GetUser().GetId()}, + }, + } + response.Administrators[0] = organizationAdmin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{organizationAdmin1.GetUser().GetId(), organizationAdmin2.GetUser().GetId(), organizationAdmin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = organizationAdmin3 + response.Administrators[1] = organizationAdmin2 + response.Administrators[2] = organizationAdmin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectAdministrator1.GetUser().GetId(), projectAdministrator2.GetUser().GetId(), projectAdministrator3.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectAdministrator3 + response.Administrators[1] = projectAdministrator2 + response.Administrators[2] = projectAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectGrantAdministrator1.GetUser().GetId(), projectGrantAdministrator2.GetUser().GetId(), projectGrantAdministrator3.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator3 + response.Administrators[1] = projectGrantAdministrator2 + response.Administrators[2] = projectGrantAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, instance owner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), organizationAdmin1.GetUser().GetId(), projectAdministrator1.GetUser().GetId(), projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + response.Administrators[1] = projectAdministrator1 + response.Administrators[2] = organizationAdmin1 + response.Administrators[3] = instanceAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, org owner", + args: args{ + ctx: organizationOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), organizationAdmin1.GetUser().GetId(), projectAdministrator1.GetUser().GetId(), projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + response.Administrators[1] = projectAdministrator1 + response.Administrators[2] = organizationAdmin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), organizationAdmin1.GetUser().GetId(), projectAdministrator1.GetUser().GetId(), projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + response.Administrators[1] = projectAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, + }, + }, + }, + { + name: "list multiple id, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), organizationAdmin1.GetUser().GetId(), projectAdministrator1.GetUser().GetId(), projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list, resource filter, instance admin", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{ + Resource: &internal_permission.ResourceFilter_Instance{Instance: true}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + Pagination: &filter.PaginationRequest{Asc: true, Limit: 2, Offset: 2}, // skip the first two, which were created + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, // 3 test admins + 2 during the integration test setup + AppliedLimit: 2, + }, + Administrators: []*internal_permission.Administrator{instanceAdministrator1, instanceAdministrator2}, + }, + }, + { + name: "list, resource filter, organization admin", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{ + Resource: &internal_permission.ResourceFilter_OrganizationId{OrganizationId: organizationResp.GetOrganizationId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + Pagination: &filter.PaginationRequest{Asc: true, Limit: 2, Offset: 1}, // skip the first as this is the instance admin creating the organization + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, // 3 test admins + 1 for testing the permissions + 1 for the instance admin creating the org + AppliedLimit: 2, + }, + Administrators: []*internal_permission.Administrator{organizationAdmin1, organizationAdmin2}, + }, + }, + { + name: "list, resource filter, project admin", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{ + Resource: &internal_permission.ResourceFilter_ProjectId{ProjectId: projectResp.GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + Pagination: &filter.PaginationRequest{Asc: true, Limit: 2}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, // 3 test admins + 1 for testing the permissions + AppliedLimit: 2, + }, + Administrators: []*internal_permission.Administrator{projectAdministrator1, projectAdministrator2}, + }, + }, + { + name: "list, resource filter, project grant admin", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{ + Resource: &internal_permission.ResourceFilter_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceFilter_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: grantedOrganizationResp.GetOrganizationId(), + }, + }, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + Pagination: &filter.PaginationRequest{Asc: true, Limit: 2}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, // 3 test admins + 1 for testing the permissions + AppliedLimit: 2, + }, + Administrators: []*internal_permission.Administrator{projectGrantAdministrator1, projectGrantAdministrator2}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instanceQuery.Client.InternalPermissionV2.ListAdministrators(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Administrators, len(tt.want.Administrators)) { + for i := range tt.want.Administrators { + // need to set the project grant ID as it is generated + if grant := got.Administrators[i].GetProjectGrant(); grant != nil { + tt.want.Administrators[i].GetProjectGrant().Id = grant.Id + } + assert.EqualExportedValues(ttt, tt.want.Administrators[i], got.Administrators[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + t.Helper() + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func createInstanceAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T) *internal_permission.Administrator { + email := integration.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateInstanceMembership(t, ctx, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Instance{ + Instance: true, + }, + Roles: []string{"IAM_OWNER"}, + } +} + +func createOrganizationAdministrator(ctx context.Context, instance *integration.Instance, organizationID, organizationName string, t *testing.T) *internal_permission.Administrator { + email := integration.Email() + + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateOrgMembership(t, ctx, organizationID, userResp.GetId(), "ORG_OWNER") + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Organization{ + Organization: &internal_permission.Organization{ + Id: organizationID, + Name: organizationName, + }, + }, + Roles: []string{"ORG_OWNER"}, + } +} + +func createProjectAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName string) *internal_permission.Administrator { + email := integration.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateProjectMembership(t, ctx, projectID, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Project{ + Project: &internal_permission.Project{ + Id: projectID, + Name: projectName, + OrganizationId: orgID, + }, + }, + Roles: []string{"PROJECT_OWNER"}, + } +} + +func createProjectGrantAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName, grantedOrgID string) *internal_permission.Administrator { + email := integration.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateProjectGrantMembership(t, ctx, projectID, grantedOrgID, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_ProjectGrant{ + ProjectGrant: &internal_permission.ProjectGrant{ + // left empty as generated + Id: "", + ProjectId: projectID, + ProjectName: projectName, + OrganizationId: orgID, + GrantedOrganizationId: grantedOrgID, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + } +} + +func TestServer_ListAdministrators_PermissionV2(t *testing.T) { + // removed as permission v2 is not implemented yet for project grant level permissions + // ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + + // instance administrators with IAM_OWNER role for tests and 3 instance administrators to query + iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + instanceAdministrator1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + instanceAdministrator2 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + instanceAdministrator3 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + + // organization setup with 3 organization administrators to query + organizationName := integration.OrganizationName() + organizationResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, organizationName, integration.Email()) + organizationAdmin1 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, organizationResp.GetOrganizationId(), organizationName, t) + organizationAdmin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, organizationResp.GetOrganizationId(), organizationName, t) + organizationAdmin3 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, organizationResp.GetOrganizationId(), organizationName, t) + + // Additionally, a machine user as a project grant owner to test permissions + userOrganizationResp := instancePermissionV2.CreateMachineUser(iamOwnerCtx) + instancePermissionV2.CreateOrgMembership(t, iamOwnerCtx, organizationResp.GetOrganizationId(), userOrganizationResp.GetUserId()) + patOrganizationResp := instancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, userOrganizationResp.GetUserId()) + organizationOwnerCtx := integration.WithAuthorizationToken(CTX, patOrganizationResp.Token) + + // project setup on the same organization with 3 project administrators to query + projectName := integration.ProjectName() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, organizationResp.GetOrganizationId(), projectName, false, false) + projectAdministrator1 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName) + projectAdministrator2 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName) + projectAdministrator3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName) + + // Additionally, a machine user as a project owner to test permissions + userProjectResp := instancePermissionV2.CreateMachineUser(iamOwnerCtx) + instancePermissionV2.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + // project grant setup on the same project with 3 project grant administrators to query + grantedOrganizationName := integration.OrganizationName() + grantedOrganizationResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, grantedOrganizationName, integration.Email()) + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrganizationResp.GetOrganizationId()) + projectGrantAdministrator1 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName, grantedOrganizationResp.GetOrganizationId()) + projectGrantAdministrator2 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName, grantedOrganizationResp.GetOrganizationId()) + projectGrantAdministrator3 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, organizationResp.GetOrganizationId(), projectResp.GetId(), projectName, grantedOrganizationResp.GetOrganizationId()) + + // Additionally, a machine user as a project grant owner to test permissions + userProjectGrantResp := instancePermissionV2.CreateMachineUser(iamOwnerCtx) + instancePermissionV2.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedOrganizationResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type args struct { + ctx context.Context + dep func(*internal_permission.ListAdministratorsRequest, *internal_permission.ListAdministratorsResponse) + req *internal_permission.ListAdministratorsRequest + } + tests := []struct { + name string + args args + want *internal_permission.ListAdministratorsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: organizationOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{ + { + Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{Ids: []string{"notexisting"}}, + }, + }, + }, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = instanceAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), instanceAdministrator2.GetUser().GetId(), instanceAdministrator3.GetUser().GetId()}, + }, + } + response.Administrators[0] = instanceAdministrator3 + response.Administrators[1] = instanceAdministrator2 + response.Administrators[2] = instanceAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{organizationAdmin1.GetUser().GetId()}, + }, + } + response.Administrators[0] = organizationAdmin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{organizationAdmin1.GetUser().GetId(), organizationAdmin2.GetUser().GetId(), organizationAdmin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = organizationAdmin3 + response.Administrators[1] = organizationAdmin2 + response.Administrators[2] = organizationAdmin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectAdministrator1.GetUser().GetId(), projectAdministrator2.GetUser().GetId(), projectAdministrator3.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectAdministrator3 + response.Administrators[1] = projectAdministrator2 + response.Administrators[2] = projectAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectGrantAdministrator1.GetUser().GetId(), projectGrantAdministrator2.GetUser().GetId(), projectGrantAdministrator3.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator3 + response.Administrators[1] = projectGrantAdministrator2 + response.Administrators[2] = projectGrantAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, instance owner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), organizationAdmin1.GetUser().GetId(), projectAdministrator1.GetUser().GetId(), projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + response.Administrators[1] = projectAdministrator1 + response.Administrators[2] = organizationAdmin1 + response.Administrators[3] = instanceAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, org owner", + args: args{ + ctx: organizationOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), organizationAdmin1.GetUser().GetId(), projectAdministrator1.GetUser().GetId(), projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + response.Administrators[1] = projectAdministrator1 + response.Administrators[2] = organizationAdmin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), organizationAdmin1.GetUser().GetId(), projectAdministrator1.GetUser().GetId(), projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + response.Administrators[1] = projectAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, + }, + }, + }, + { + name: "list multiple id, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{instanceAdministrator1.GetUser().GetId(), organizationAdmin1.GetUser().GetId(), projectAdministrator1.GetUser().GetId(), projectGrantAdministrator1.GetUser().GetId()}, + }, + } + response.Administrators[0] = projectGrantAdministrator1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{{}}, + }, + }, + { + name: "list, resource filter, instance admin", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{ + Resource: &internal_permission.ResourceFilter_Instance{Instance: true}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + Pagination: &filter.PaginationRequest{Asc: true, Limit: 2, Offset: 2}, // skip the first two, which were created + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, // 3 test admins + 2 during the integration test setup + AppliedLimit: 2, + }, + Administrators: []*internal_permission.Administrator{instanceAdministrator1, instanceAdministrator2}, + }, + }, + { + name: "list, resource filter, organization admin", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{ + Resource: &internal_permission.ResourceFilter_OrganizationId{OrganizationId: organizationResp.GetOrganizationId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + Pagination: &filter.PaginationRequest{Asc: true, Limit: 2, Offset: 1}, // skip the first as this is the instance admin creating the organization + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, // 3 test admins + 1 for testing the permissions + 1 for the instance admin creating the org + AppliedLimit: 2, + }, + Administrators: []*internal_permission.Administrator{organizationAdmin1, organizationAdmin2}, + }, + }, + { + name: "list, resource filter, project admin", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{ + Resource: &internal_permission.ResourceFilter_ProjectId{ProjectId: projectResp.GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + Pagination: &filter.PaginationRequest{Asc: true, Limit: 2}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, // 3 test admins + 1 for testing the permissions + AppliedLimit: 2, + }, + Administrators: []*internal_permission.Administrator{projectAdministrator1, projectAdministrator2}, + }, + }, + { + name: "list, resource filter, project grant admin", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_Resource{ + Resource: &internal_permission.ResourceFilter{ + Resource: &internal_permission.ResourceFilter_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceFilter_ProjectGrant{ + ProjectId: projectResp.GetId(), + OrganizationId: grantedOrganizationResp.GetOrganizationId(), + }, + }, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + Pagination: &filter.PaginationRequest{Asc: true, Limit: 2}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, // 3 test admins + 1 for testing the permissions + AppliedLimit: 2, + }, + Administrators: []*internal_permission.Administrator{projectGrantAdministrator1, projectGrantAdministrator2}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.InternalPermissionV2.ListAdministrators(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Administrators, len(tt.want.Administrators)) { + for i := range tt.want.Administrators { + // set as project grant id is generated + if grant := got.Administrators[i].GetProjectGrant(); grant != nil { + tt.want.Administrators[i].GetProjectGrant().Id = grant.Id + } + assert.EqualExportedValues(ttt, tt.want.Administrators[i], got.Administrators[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} diff --git a/internal/api/grpc/internal_permission/v2/integration_test/server_test.go b/internal/api/grpc/internal_permission/v2/integration_test/server_test.go new file mode 100644 index 00000000000..be77e7847a5 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2/integration_test/server_test.go @@ -0,0 +1,65 @@ +//go:build integration + +package project_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +var ( + CTX context.Context + instance *integration.Instance + instanceQuery *integration.Instance + instancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + instance = integration.NewInstance(ctx) + instanceQuery = integration.NewInstance(ctx) // create a separate one for queries to avoid side effects + instancePermissionV2 = integration.NewInstance(CTX) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + assert.NoError(ttt, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/internal_permission/v2/query.go b/internal/api/grpc/internal_permission/v2/query.go new file mode 100644 index 00000000000..c90593813c1 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2/query.go @@ -0,0 +1,208 @@ +package internal_permission + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2" +) + +func (s *Server) ListAdministrators(ctx context.Context, req *connect.Request[internal_permission.ListAdministratorsRequest]) (*connect.Response[internal_permission.ListAdministratorsResponse], error) { + queries, err := s.listAdministratorsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchAdministrators(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&internal_permission.ListAdministratorsResponse{ + Administrators: administratorsToPb(resp.Administrators), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listAdministratorsRequestToModel(req *internal_permission.ListAdministratorsRequest) (*query.MembershipSearchQuery, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := administratorSearchFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.MembershipSearchQuery{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: administratorFieldNameToSortingColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func administratorFieldNameToSortingColumn(field internal_permission.AdministratorFieldName) query.Column { + switch field { + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_CREATION_DATE: + return query.MembershipCreationDate + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_USER_ID: + return query.MembershipUserID + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_CHANGE_DATE: + return query.MembershipChangeDate + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_UNSPECIFIED: + return query.MembershipCreationDate + default: + return query.MembershipCreationDate + } +} + +func administratorSearchFiltersToQuery(queries []*internal_permission.AdministratorSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = administratorFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func administratorFilterToModel(filter *internal_permission.AdministratorSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *internal_permission.AdministratorSearchFilter_InUserIdsFilter: + return inUserIDsFilterToQuery(q.InUserIdsFilter) + case *internal_permission.AdministratorSearchFilter_CreationDate: + return creationDateFilterToQuery(q.CreationDate) + case *internal_permission.AdministratorSearchFilter_ChangeDate: + return changeDateFilterToQuery(q.ChangeDate) + case *internal_permission.AdministratorSearchFilter_UserOrganizationId: + return userResourceOwnerFilterToQuery(q.UserOrganizationId) + case *internal_permission.AdministratorSearchFilter_UserPreferredLoginName: + return userLoginNameFilterToQuery(q.UserPreferredLoginName) + case *internal_permission.AdministratorSearchFilter_UserDisplayName: + return userDisplayNameFilterToQuery(q.UserDisplayName) + case *internal_permission.AdministratorSearchFilter_Resource: + return resourceFilterToQuery(q.Resource) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func inUserIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewMemberInUserIDsSearchQuery(q.GetIds()) +} + +func userResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserResourceOwnerSearchQuery(q.GetId()) +} + +func userLoginNameFilterToQuery(q *internal_permission.UserPreferredLoginNameFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserLoginNameSearchQuery(q.GetPreferredLoginName()) +} + +func userDisplayNameFilterToQuery(q *internal_permission.UserDisplayNameFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserDisplayNameSearchQuery(q.GetDisplayName()) +} + +func creationDateFilterToQuery(q *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewMembershipCreationDateQuery(q.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(q.Method)) +} + +func changeDateFilterToQuery(q *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewMembershipChangeDateQuery(q.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(q.Method)) +} + +func resourceFilterToQuery(q *internal_permission.ResourceFilter) (query.SearchQuery, error) { + switch r := q.GetResource().(type) { + case *internal_permission.ResourceFilter_Instance: + if q.GetInstance() { + return query.NewMembershipIsIAMQuery() + } + case *internal_permission.ResourceFilter_OrganizationId: + return query.NewMembershipOrgIDQuery(r.OrganizationId) + case *internal_permission.ResourceFilter_ProjectId: + projectIDQuery, err := query.NewMembershipProjectIDQuery(r.ProjectId) + if err != nil { + return nil, err + } + notGrantedQuery, err := query.NewMembershipNotGrantedSearchQuery() + if err != nil { + return nil, err + } + return query.NewAndQuery(projectIDQuery, notGrantedQuery) + case *internal_permission.ResourceFilter_ProjectGrant_: + projectIDQuery, err := query.NewMembershipProjectIDQuery(r.ProjectGrant.GetProjectId()) + if err != nil { + return nil, err + } + grantedOrganizationID, err := query.NewMembershipGrantedOrgIDSearchQuery(r.ProjectGrant.GetOrganizationId()) + if err != nil { + return nil, err + } + return query.NewAndQuery(projectIDQuery, grantedOrganizationID) + } + return nil, nil +} + +func administratorsToPb(administrators []*query.Administrator) []*internal_permission.Administrator { + a := make([]*internal_permission.Administrator, len(administrators)) + for i, admin := range administrators { + a[i] = administratorToPb(admin) + } + return a +} + +func administratorToPb(admin *query.Administrator) *internal_permission.Administrator { + var resource internal_permission.Resource + if admin.Instance != nil { + resource = &internal_permission.Administrator_Instance{Instance: true} + } + if admin.Org != nil { + resource = &internal_permission.Administrator_Organization{ + Organization: &internal_permission.Organization{ + Id: admin.Org.OrgID, + Name: admin.Org.Name, + }, + } + } + if admin.Project != nil { + resource = &internal_permission.Administrator_Project{ + Project: &internal_permission.Project{ + Id: admin.Project.ProjectID, + Name: admin.Project.Name, + OrganizationId: admin.Project.ResourceOwner, + }, + } + } + if admin.ProjectGrant != nil { + resource = &internal_permission.Administrator_ProjectGrant{ + ProjectGrant: &internal_permission.ProjectGrant{ + Id: admin.ProjectGrant.GrantID, + ProjectId: admin.ProjectGrant.ProjectID, + ProjectName: admin.ProjectGrant.ProjectName, + OrganizationId: admin.ProjectGrant.ResourceOwner, + GrantedOrganizationId: admin.ProjectGrant.GrantedOrgID, + }, + } + } + + return &internal_permission.Administrator{ + CreationDate: timestamppb.New(admin.CreationDate), + ChangeDate: timestamppb.New(admin.ChangeDate), + User: &internal_permission.User{ + Id: admin.User.UserID, + PreferredLoginName: admin.User.LoginName, + DisplayName: admin.User.DisplayName, + OrganizationId: admin.User.ResourceOwner, + }, + Resource: resource, + Roles: admin.Roles, + } +} diff --git a/internal/api/grpc/internal_permission/v2/server.go b/internal/api/grpc/internal_permission/v2/server.go new file mode 100644 index 00000000000..1df5e70c702 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2/server.go @@ -0,0 +1,59 @@ +package internal_permission + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2" + "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2/internal_permissionconnect" +) + +var _ internal_permissionconnect.InternalPermissionServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + checkPermission domain.PermissionCheck +} + +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, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return internal_permissionconnect.NewInternalPermissionServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return internal_permission.File_zitadel_internal_permission_v2_internal_permission_service_proto +} + +func (s *Server) AppName() string { + return internal_permission.InternalPermissionService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return internal_permission.InternalPermissionService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return internal_permission.InternalPermissionService_AuthMethods +} diff --git a/internal/integration/client.go b/internal/integration/client.go index d6dc45a9dac..92393633097 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -33,6 +33,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/idp" idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + internal_permission_v2 "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2" internal_permission_v2beta "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" @@ -86,6 +87,7 @@ type Client struct { InstanceV2Beta instance.InstanceServiceClient AppV2Beta app.AppServiceClient InternalPermissionv2Beta internal_permission_v2beta.InternalPermissionServiceClient + InternalPermissionV2 internal_permission_v2.InternalPermissionServiceClient AuthorizationV2Beta authorization.AuthorizationServiceClient GroupV2 group_v2.GroupServiceClient } @@ -131,6 +133,7 @@ func newClient(ctx context.Context, target string) (*Client, error) { InstanceV2Beta: instance.NewInstanceServiceClient(cc), AppV2Beta: app.NewAppServiceClient(cc), InternalPermissionv2Beta: internal_permission_v2beta.NewInternalPermissionServiceClient(cc), + InternalPermissionV2: internal_permission_v2.NewInternalPermissionServiceClient(cc), AuthorizationV2Beta: authorization.NewAuthorizationServiceClient(cc), GroupV2: group_v2.NewGroupServiceClient(cc), } diff --git a/internal/query/administrators.go b/internal/query/administrators.go index 0128c0f25e5..1164d010f71 100644 --- a/internal/query/administrators.go +++ b/internal/query/administrators.go @@ -192,7 +192,6 @@ func (q *Queries) searchAdministrators(ctx context.Context, queries *MembershipS return nil, err } queryArgs = append(queryArgs, args...) - err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { administrators, err = scan(rows) return err diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index 069210a8306..e18a4cc8b04 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -90,6 +90,10 @@ func NewMembershipGrantedOrgIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(ProjectGrantColumnGrantedOrgID, id, TextEquals) } +func NewMembershipNotGrantedSearchQuery() (SearchQuery, error) { + return NewIsNullQuery(ProjectGrantColumnGrantedOrgID) +} + func NewMembershipProjectIDQuery(value string) (SearchQuery, error) { return NewTextQuery(ProjectMemberProjectID, value, TextEquals) } diff --git a/pkg/grpc/internal_permission/v2/resource.go b/pkg/grpc/internal_permission/v2/resource.go new file mode 100644 index 00000000000..a57f60242e8 --- /dev/null +++ b/pkg/grpc/internal_permission/v2/resource.go @@ -0,0 +1,3 @@ +package internal_permission + +type Resource = isAdministrator_Resource diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 1a9bd8558b6..f9976810328 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -4050,7 +4050,7 @@ service AdminService { // List IAM Members // - // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-list-administrators.api.mdx) instead. + // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-list-administrators.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request returns all users with memberships on the instance level, matching the search queries. The search queries will be AND linked. rpc ListIAMMembers(ListIAMMembersRequest) returns (ListIAMMembersResponse) { @@ -4078,7 +4078,7 @@ service AdminService { // Add IAM Member // - // Deprecated: use [CreateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-create-administrator.api.mdx) instead. + // Deprecated: use [CreateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-create-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request adds a new user to the members list with one or multiple roles. rpc AddIAMMember(AddIAMMemberRequest) returns (AddIAMMemberResponse) { @@ -4117,7 +4117,7 @@ service AdminService { // Update IAM Member // - // Deprecated: use [UpdateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-update-administrator.api.mdx) instead. + // Deprecated: use [UpdateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-update-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request changes the roles of an existing member. The whole roles list will be updated. Make sure to include roles that you don't want to change (remove). rpc UpdateIAMMember(UpdateIAMMemberRequest) returns (UpdateIAMMemberResponse) { @@ -4156,7 +4156,7 @@ service AdminService { // Remove IAM Member // - // Deprecated: use [DeleteAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-delete-administrator.api.mdx) instead. + // Deprecated: use [DeleteAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-delete-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request removes a user from the members list on an instance level. The user can still have roles on another level (organization, project). rpc RemoveIAMMember(RemoveIAMMemberRequest) returns (RemoveIAMMemberResponse) { diff --git a/proto/zitadel/internal_permission/v2/internal_permission_service.proto b/proto/zitadel/internal_permission/v2/internal_permission_service.proto new file mode 100644 index 00000000000..340e33fe27d --- /dev/null +++ b/proto/zitadel/internal_permission/v2/internal_permission_service.proto @@ -0,0 +1,236 @@ +syntax = "proto3"; + +package zitadel.internal_permission.v2; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; +import "zitadel/internal_permission/v2/query.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2;internal_permission"; + +// InternalPermissionService provides methods to manage permissions for resource +// and their management in ZITADEL itself also known as "administrators". +// +// If you want to manage permissions and roles within your project or application, +// please use the AuthorizationsService. +service InternalPermissionService { + + // List Administrators + // + // ListAdministrators returns all administrators and their roles matching the request and the caller's permissions to retrieve. + // + // Required permissions depend on the resource type: + // - "iam.member.read" for instance administrators + // - "org.member.read" for organization administrators + // - "project.member.read" for project administrators + // - "project.grant.member.read" for project grant administrators + // - no permissions required for listing own administrator roles + rpc ListAdministrators(ListAdministratorsRequest) returns (ListAdministratorsResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: {permission: "authenticated"} + }; + } + + // Create Administrator + // + // CreateAdministrator grants an administrator role to a user for a specific resource. + // + // Note that the roles are specific to the resource type. + // This means that if you want to grant a user the administrator role for an organization and a project, + // you need to create two administrator roles. + // + // Required permissions depend on the resource type: + // - "iam.member.write" for instance administrators + // - "org.member.write" for organization administrators + // - "project.member.write" for project administrators + // - "project.grant.member.write" for project grant administrators + rpc CreateAdministrator(CreateAdministratorRequest) returns (CreateAdministratorResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: {permission: "authenticated"} + }; + } + + // Update Administrator + // + // UpdateAdministrator updates the specific administrator role. + // + // Note that any role previously granted to the user and not present in the request will be revoked. + // + // Required permissions depend on the resource type: + // - "iam.member.write" for instance administrators + // - "org.member.write" for organization administrators + // - "project.member.write" for project administrators + // - "project.grant.member.write" for project grant administrators + rpc UpdateAdministrator(UpdateAdministratorRequest) returns (UpdateAdministratorResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: {permission: "authenticated"} + }; + } + + // Delete Administrator + // + // DeleteAdministrator revokes an administrator role from a user. + // + // In case the administrator role is not found, the request will return a successful response as + // the desired state is already achieved. + // You can check the deletion date in the response to verify if the administrator role was deleted during the request. + // + // Required permissions depend on the resource type: + // - "iam.member.delete" for instance administrators + // - "org.member.delete" for organization administrators + // - "project.member.delete" for project administrators + // - "project.grant.member.delete" for project grant administrators + rpc DeleteAdministrator(DeleteAdministratorRequest) returns (DeleteAdministratorResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: {permission: "authenticated"} + }; + } +} + +message ListAdministratorsRequest { + // Paginate through the results using a limit, offset and sorting. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + + // The field the result is sorted by. The default is the creation date. + // Beware that if you change this, your result pagination might be inconsistent. + AdministratorFieldName sorting_column = 2; + + // Filter the administrator roles to be returned. + repeated AdministratorSearchFilter filters = 3; +} + +message ListAdministratorsResponse { + // Pagination contains the current sequence and the total result count. + zitadel.filter.v2.PaginationResponse pagination = 1; + + // Administrators contains the list of administrators matching the request. + repeated Administrator administrators = 2; +} + +message CreateAdministratorRequest { + // UserID is the ID of the user who should be granted the administrator role. + string user_id = 1 [ + (validate.rules).string = { + min_len: 1 + max_len: 200 + }, + (google.api.field_behavior) = REQUIRED + ]; + + // Resource is the type of the resource the administrator roles should be granted for. + ResourceType resource = 2 [(google.api.field_behavior) = REQUIRED]; + + // Roles are the roles that should be granted to the user for the specified resource. + // Note that roles are currently specific to the resource type. + // This means that if you want to grant a user the administrator role for an organization and a project, + // you need to create two administrator roles. + repeated string roles = 3 [ + (validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }, + (google.api.field_behavior) = REQUIRED + ]; +} + +message ResourceType { + message ProjectGrant { + // ProjectID is the unique identifier of the project the project grant belongs to. + string project_id = 1 [ + (validate.rules).string = { + min_len: 1 + max_len: 200 + }, + (google.api.field_behavior) = REQUIRED + ]; + + // OrganizationID is the unique identifier of the organization the project was granted to + // and on which the administrator role should be granted. + string organization_id = 2 [ + (validate.rules).string = { + min_len: 1 + max_len: 200 + }, + (google.api.field_behavior) = REQUIRED + ]; + } + + // Resource is the type of the resource the administrator roles should be granted for. + oneof resource { + option (validate.required) = true; + + // Instance is the resource type for granting administrator privileges on the instance level. + bool instance = 1 [(validate.rules).bool = {const: true}]; + + // OrganizationID is required to grant administrator privileges for a specific organization. + string organization_id = 2; + + // ProjectID is required to grant administrator privileges for a specific project. + string project_id = 3; + + // ProjectGrantID is required to grant administrator privileges for a specific project grant. + ProjectGrant project_grant = 4; + } +} + +message CreateAdministratorResponse { + // CreationDate is the timestamp when the administrator role was created. + google.protobuf.Timestamp creation_date = 1; +} + +message UpdateAdministratorRequest { + // UserID is the ID of the user whose administrator roles should be updated. + string user_id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + + // Resource is the type of the resource the administrator roles should be granted for. + ResourceType resource = 2 [(google.api.field_behavior) = REQUIRED]; + + // Roles are the roles that the user should be granted. + // Note that any role previously granted to the user and not present in the list will be revoked. + repeated string roles = 3 [(validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }]; +} + +message UpdateAdministratorResponse { + // ChangeDate is the timestamp when the administrator role was last updated. + google.protobuf.Timestamp change_date = 1; +} + +message DeleteAdministratorRequest { + // UserID is the ID of the user whose administrator roles should be removed. + string user_id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + + // Resource is the type of the resource the administrator roles should be removed for. + ResourceType resource = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message DeleteAdministratorResponse { + // DeletionDate is the timestamp when the administrator role was deleted. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might not be set. + google.protobuf.Timestamp deletion_date = 1; +} diff --git a/proto/zitadel/internal_permission/v2/query.proto b/proto/zitadel/internal_permission/v2/query.proto new file mode 100644 index 00000000000..3aaca6f11f9 --- /dev/null +++ b/proto/zitadel/internal_permission/v2/query.proto @@ -0,0 +1,234 @@ +syntax = "proto3"; + +package zitadel.internal_permission.v2; + +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2;internal_permission"; + +message Administrator { + // CreationDate is the timestamp when the administrator role was granted. + google.protobuf.Timestamp creation_date = 1; + + // ChangeDate is the timestamp when the administrator role was last updated. + // In case the administrator role was not updated, this field is equal to the creation date. + google.protobuf.Timestamp change_date = 2; + + // User is the user who was granted the administrator role. + User user = 3; + + // Resource is the type of the resource the administrator roles were granted for. + oneof resource { + // Instance is returned if the administrator roles were granted on the instance level. + bool instance = 4; + + // Organization provides information about the organization the administrator roles were granted for. + Organization organization = 5; + + // Project provides information about the project the administrator roles were granted for. + Project project = 6; + + // ProjectGrant provides information about the project grant the administrator roles were granted for. + ProjectGrant project_grant = 7; + } + + // Roles are the roles that were granted to the user for the specified resource. + repeated string roles = 8; +} + +message User { + // ID is the unique identifier of the user. + string id = 1; + + // PreferredLoginName is the preferred login name of the user. This value is unique across the whole instance. + string preferred_login_name = 2; + + // DisplayName is the public display name of the user. + // By default it's the user's given name and family name, their username or their email address. + string display_name = 3; + + // The organization the user belong to. + string organization_id = 4; +} + +message Organization { + // ID is the unique identifier of the organization the user was granted the administrator role for. + string id = 1; + + // Name is the name of the organization the user was granted the administrator role for. + string name = 2; +} + +message Project { + // ID is the unique identifier of the project the user was granted the administrator role for. + string id = 1; + + // Name is the name of the project the user was granted the administrator role for. + string name = 2; + + // OrganizationID is the ID of the organization the project belongs to. + string organization_id = 3; +} + +message ProjectGrant { + // ID is the unique identifier of the project grant the user was granted the administrator role for. + string id = 1; + + // ProjectID is the ID of the project the project grant belongs to. + string project_id = 2; + + // ProjectName is the name of the project the project grant belongs to. + string project_name = 3; + + // OrganizationID is the ID of the organization the project belongs to. + string organization_id = 4; + + // GrantedOrganizationID is the ID of the organization the project grant belongs to. + string granted_organization_id = 5; +} + +message AdministratorSearchFilter { + oneof filter { + option (validate.required) = true; + + // Search for administrator roles by their creation date. + zitadel.filter.v2.TimestampFilter creation_date = 1; + + // Search for administrator roles by their change date. + zitadel.filter.v2.TimestampFilter change_date = 2; + + // Search for administrators roles by the IDs of the users who was granted the administrator role. + zitadel.filter.v2.InIDsFilter in_user_ids_filter = 3; + + // Search for administrators roles by the ID of the organization the user is part of. + zitadel.filter.v2.IDFilter user_organization_id = 4; + + // Search for administrators roles by the preferred login name of the user. + UserPreferredLoginNameFilter user_preferred_login_name = 5; + + // Search for administrators roles by the display name of the user. + UserDisplayNameFilter user_display_name = 6; + + // Search for administrators roles granted for a specific resource. + ResourceFilter resource = 7; + + // Search for administrators roles granted with a specific role. + RoleFilter role = 8; + + // Combine multiple authorization queries with an AND operation. + AndFilter and = 9; + + // Combine multiple authorization queries with an OR operation. + // For example, to search for authorizations of multiple OrganizationIDs. + OrFilter or = 10; + + // Negate an authorization query. + NotFilter not = 11; + } +} + +message UserPreferredLoginNameFilter { + // Search for administrators by the preferred login name of the user. + string preferred_login_name = 1 [ + (validate.rules).string = { + min_len: 1 + max_len: 200 + }, + (google.api.field_behavior) = REQUIRED + ]; + + // Specify the method to search for the preferred login name. Default is EQUAL. + // For example, to search for all administrator roles of a user with a preferred login name + // containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message UserDisplayNameFilter { + // Search for administrators by the display name of the user. + string display_name = 1 [ + (validate.rules).string = { + min_len: 1 + max_len: 200 + }, + (google.api.field_behavior) = REQUIRED + ]; + + // Specify the method to search for the display name. Default is EQUAL. + // For example, to search for all administrator roles of a user with a display name + // containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message ResourceFilter { + message ProjectGrant { + // ProjectID is the unique identifier of the project the project grant belongs to. + string project_id = 1 [ + (validate.rules).string = { + min_len: 1 + max_len: 200 + }, + (google.api.field_behavior) = REQUIRED + ]; + + // OrganizationID is the unique identifier of the organization the project was granted to + // and on which the administrator role was granted too. + string organization_id = 2 [ + (validate.rules).string = { + min_len: 1 + max_len: 200 + }, + (google.api.field_behavior) = REQUIRED + ]; + } + + // Search for administrators by the granted resource. + oneof resource { + option (validate.required) = true; + + // Search for administrators granted on the instance level. + bool instance = 1 [(validate.rules).bool = {const: true}]; + + // Search for administrators granted on a specific organization. + string organization_id = 2; + + // Search for administrators granted on a specific project. + string project_id = 3; + + // Search for administrators granted on a specific project grant + // by providing both the project ID and the organization ID. + ProjectGrant project_grant = 4; + } +} + +message RoleFilter { + // Search for administrators by the granted role. + string role_key = 1 [ + (validate.rules).string = { + min_len: 1 + max_len: 200 + }, + (google.api.field_behavior) = REQUIRED + ]; +} + +message AndFilter { + repeated AdministratorSearchFilter queries = 1; +} + +message OrFilter { + repeated AdministratorSearchFilter queries = 1; +} + +message NotFilter { + AdministratorSearchFilter query = 1; +} + +enum AdministratorFieldName { + ADMINISTRATOR_FIELD_NAME_UNSPECIFIED = 0; + ADMINISTRATOR_FIELD_NAME_USER_ID = 1; + ADMINISTRATOR_FIELD_NAME_CREATION_DATE = 2; + ADMINISTRATOR_FIELD_NAME_CHANGE_DATE = 3; +} diff --git a/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto b/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto index 977d95fedae..b36433d40d4 100644 --- a/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto +++ b/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto @@ -111,9 +111,13 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { // // If you want to manage permissions and roles within your project or application, // please use the AuthorizationsService. +// +// Deprecated: use internal permission service v2 instead. This service will be removed in the next major version of ZITADEL. service InternalPermissionService { // ListAdministrators returns all administrators and its roles matching the request and necessary permissions. // + // Deprecated: please move to the corresponding endpoint under internal permission service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Required permissions depend on the resource type: // - "iam.member.read" for instance administrators // - "org.member.read" for organization administrators @@ -133,6 +137,7 @@ service InternalPermissionService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -150,6 +155,8 @@ service InternalPermissionService { // CreateAdministrator grants a administrator role to a user for a specific resource. // + // Deprecated: please move to the corresponding endpoint under internal permission service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Note that the roles are specific to the resource type. // This means that if you want to grant a user the administrator role for an organization and a project, // you need to create two administrator roles. @@ -172,6 +179,7 @@ service InternalPermissionService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -189,6 +197,8 @@ service InternalPermissionService { // UpdateAdministrator updates the specific administrator role. // + // Deprecated: please move to the corresponding endpoint under internal permission service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Note that any role previously granted to the user and not present in the request will be revoked. // // Required permissions depend on the resource type: @@ -209,6 +219,7 @@ service InternalPermissionService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -226,6 +237,8 @@ service InternalPermissionService { // DeleteAdministrator revokes a administrator role from a user. // + // Deprecated: please move to the corresponding endpoint under internal permission service v2. This endpoint will be removed with the next major version of ZITADEL. + // // In case the administrator role is not found, the request will return a successful response as // the desired state is already achieved. // You can check the deletion date in the response to verify if the administrator role was deleted during the request. @@ -247,6 +260,7 @@ service InternalPermissionService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 19dfa9153a7..31ce827f872 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2185,7 +2185,7 @@ service ManagementService { // List ZITADEL Permissions // - // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-list-administrators.api.mdx) instead. + // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-list-administrators.api.mdx) instead. // // Show all the permissions the user has in ZITADEL (ZITADEL Manager). rpc ListUserMemberships(ListUserMembershipsRequest) returns (ListUserMembershipsResponse) { @@ -2799,7 +2799,7 @@ service ManagementService { // List Organization Members // - // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-list-administrators.api.mdx) instead. + // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-list-administrators.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request returns all users with memberships on the organization level, matching the search queries. The search queries will be AND linked. rpc ListOrgMembers(ListOrgMembersRequest) returns (ListOrgMembersResponse) { @@ -2830,7 +2830,7 @@ service ManagementService { // Add Organization Member // - // Deprecated: use [CreateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-create-administrator.api.mdx) instead. + // Deprecated: use [CreateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-create-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request adds a new user to the members list on the organization level with one or multiple roles. rpc AddOrgMember(AddOrgMemberRequest) returns (AddOrgMemberResponse) { @@ -2861,7 +2861,7 @@ service ManagementService { // Update Organization Member // - // Deprecated: use [UpdateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-update-administrator.api.mdx) instead. + // Deprecated: use [UpdateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-update-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request changes the roles of an existing member. The whole roles list will be updated. Make sure to include roles that you don't want to change (remove). rpc UpdateOrgMember(UpdateOrgMemberRequest) returns (UpdateOrgMemberResponse) { @@ -2892,7 +2892,7 @@ service ManagementService { // Remove Organization Member // - // Deprecated: use [DeleteAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-delete-administrator.api.mdx) instead. + // Deprecated: use [DeleteAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-delete-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request removes a user from the members list on an instance level. The user can still have roles on another level (iam, project). rpc RemoveOrgMember(RemoveOrgMemberRequest) returns (RemoveOrgMemberResponse) { @@ -3415,7 +3415,7 @@ service ManagementService { // List Project Members // - // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-list-administrators.api.mdx) instead. + // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-list-administrators.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request returns all users with memberships on the project level, matching the search queries. The search queries will be AND linked. rpc ListProjectMembers(ListProjectMembersRequest) returns (ListProjectMembersResponse) { @@ -3447,7 +3447,7 @@ service ManagementService { // Add Project Member // - // Deprecated: use [CreateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-create-administrator.api.mdx) instead. + // Deprecated: use [CreateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-create-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request adds a new user to the members list on the project level with one or multiple roles. rpc AddProjectMember(AddProjectMemberRequest) returns (AddProjectMemberResponse) { @@ -3479,7 +3479,7 @@ service ManagementService { // Update Project Member // - // Deprecated: use [UpdateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-update-administrator.api.mdx) instead. + // Deprecated: use [UpdateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-update-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request changes the roles of an existing member. The whole roles list will be updated. Make sure to include roles that you don't want to change (remove). rpc UpdateProjectMember(UpdateProjectMemberRequest) returns (UpdateProjectMemberResponse) { @@ -3511,7 +3511,7 @@ service ManagementService { // Remove Project Member // - // Deprecated: use [DeleteAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-delete-administrator.api.mdx) instead. + // Deprecated: use [DeleteAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-delete-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request removes a user from the members list on an project level. The user can still have roles on another level (iam, organization). rpc RemoveProjectMember(RemoveProjectMemberRequest) returns (RemoveProjectMemberResponse) { @@ -4386,7 +4386,7 @@ service ManagementService { // List Project Grant Members // - // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-list-administrators.api.mdx) instead. + // Deprecated: use [ListAdministrators](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-list-administrators.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request returns all users with memberships on the project grant level, matching the search queries. The search queries will be AND linked. rpc ListProjectGrantMembers(ListProjectGrantMembersRequest) returns (ListProjectGrantMembersResponse) { @@ -4417,7 +4417,7 @@ service ManagementService { // Add Project Grant Member // - // Deprecated: use [CreateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-create-administrator.api.mdx) instead. + // Deprecated: use [CreateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-create-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request returns all users with memberships on the project grant level, matching the search queries. The search queries will be AND linked. rpc AddProjectGrantMember(AddProjectGrantMemberRequest) returns (AddProjectGrantMemberResponse) { @@ -4448,7 +4448,7 @@ service ManagementService { // Update Project Grant Member // - // Deprecated: use [UpdateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-update-administrator.api.mdx) instead. + // Deprecated: use [UpdateAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-update-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request changes the roles of an existing member. The whole roles list will be updated. Make sure to include roles that you don't want to change (remove). rpc UpdateProjectGrantMember(UpdateProjectGrantMemberRequest) returns (UpdateProjectGrantMemberResponse) { @@ -4479,7 +4479,7 @@ service ManagementService { // Remove Project Grant Member // - // Deprecated: use [DeleteAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-beta-internal-permission-service-delete-administrator.api.mdx) instead. + // Deprecated: use [DeleteAdministrator](apis/resources/internal_permission_service_v2/zitadel-internal-permission-v-2-internal-permission-service-delete-administrator.api.mdx) instead. // // Members are users with permission to administrate ZITADEL on different levels. This request removes a user from the members list on a project grant level. The user can still have roles on another level (iam, organization, project). rpc RemoveProjectGrantMember(RemoveProjectGrantMemberRequest) returns (RemoveProjectGrantMemberResponse) {