diff --git a/app/finders/clusters/agents/authorizations/user_access/finder.rb b/app/finders/clusters/agents/authorizations/user_access/finder.rb index 6bd0c226337b9429289eb473e2b4d339921b46ad..bde75fa46cb0cea51cfde4e3adde59e657e78bee 100644 --- a/app/finders/clusters/agents/authorizations/user_access/finder.rb +++ b/app/finders/clusters/agents/authorizations/user_access/finder.rb @@ -5,9 +5,12 @@ module Agents module Authorizations module UserAccess class Finder - def initialize(user, agent:) + def initialize(user, agent: nil, project: nil, preload: true, limit: nil) @user = user @agent = agent + @project = project + @limit = limit + @preload = preload end def execute @@ -16,19 +19,23 @@ def execute private - attr_reader :user, :agent + attr_reader :user, :agent, :project, :preload, :limit def project_authorizations authorizations = Clusters::Agents::Authorizations::UserAccess::ProjectAuthorization.for_user(user) authorizations = filter_by_agent(authorizations) - authorizations = preload(authorizations) + authorizations = filter_by_project(authorizations) + authorizations = apply_limit(authorizations) + authorizations = apply_preload(authorizations) authorizations.to_a end def group_authorizations authorizations = Clusters::Agents::Authorizations::UserAccess::GroupAuthorization.for_user(user) authorizations = filter_by_agent(authorizations) - authorizations = preload(authorizations) + authorizations = filter_by_project(authorizations) + authorizations = apply_limit(authorizations) + authorizations = apply_preload(authorizations) authorizations.to_a end @@ -38,7 +45,21 @@ def filter_by_agent(authorizations) authorizations.for_agent(agent) end - def preload(authorizations) + def filter_by_project(authorizations) + return authorizations unless project.present? + + authorizations.for_project(project) + end + + def apply_limit(authorizations) + return authorizations unless limit.present? + + authorizations.limit(limit) + end + + def apply_preload(authorizations) + return authorizations unless preload + authorizations.preloaded end end diff --git a/app/graphql/resolvers/clusters/agents/authorizations/user_access_resolver.rb b/app/graphql/resolvers/clusters/agents/authorizations/user_access_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..280db570aa6f1614436554ae1f05ac0b95f2e6b6 --- /dev/null +++ b/app/graphql/resolvers/clusters/agents/authorizations/user_access_resolver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + module Clusters + module Agents + module Authorizations + class UserAccessResolver < BaseResolver + type Types::Clusters::Agents::Authorizations::UserAccessType, null: true + + alias_method :project, :object + + def resolve(*) + ::Clusters::Agents::Authorizations::UserAccess::Finder.new(current_user, project: project).execute + end + end + end + end + end +end diff --git a/app/graphql/types/clusters/agents/authorizations/user_access_type.rb b/app/graphql/types/clusters/agents/authorizations/user_access_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c5a466cde2c1e17fd332e5515625ff2da400567 --- /dev/null +++ b/app/graphql/types/clusters/agents/authorizations/user_access_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module Clusters + module Agents + module Authorizations + class UserAccessType < BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'ClusterAgentAuthorizationUserAccess' + + field :agent, Types::Clusters::AgentType, + description: 'Authorized cluster agent.', + null: true + + field :config, GraphQL::Types::JSON, # rubocop:disable Graphql/JSONType + description: 'Configuration for the authorized project.', + null: true + end + end + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index a0f9864351c6a80a328e3876ebb120a49bb282d4..b10b4621534a8647532b091c62bc24125500085a 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -528,6 +528,12 @@ class ProjectType < BaseObject resolver: ::Resolvers::Clusters::Agents::Authorizations::CiAccessResolver, authorize: :read_cluster_agent + field :user_access_authorized_agents, ::Types::Clusters::Agents::Authorizations::UserAccessType.connection_type, + null: true, + description: 'Authorized cluster agents for the project through user_access keyword.', + resolver: ::Resolvers::Clusters::Agents::Authorizations::UserAccessResolver, + authorize: :read_cluster_agent + field :merge_commit_template, GraphQL::Types::String, null: true, description: 'Template used to create merge commit message in merge requests.' diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb index b5d634927e5775e83627aca5a93a2fca9044db50..6980ec1c2d3a3675590e03431cdce0b410d5930b 100644 --- a/app/models/clusters/agent.rb +++ b/app/models/clusters/agent.rb @@ -71,6 +71,14 @@ def ci_access_authorized_for?(user) ).exists? end + def user_access_authorized_for?(user) + return false unless user + return false unless ::Feature.enabled?(:expose_authorized_cluster_agents, project) + + Clusters::Agents::Authorizations::UserAccess::Finder + .new(user, agent: self, preload: false, limit: 1).execute.any? + end + # As of today, all config values of associated authorization rows have the same value. # See `UserAccess::RefreshService` for more information. def user_access_config diff --git a/app/models/clusters/agents/authorizations/user_access/group_authorization.rb b/app/models/clusters/agents/authorizations/user_access/group_authorization.rb index 486a6011d84924865d434f66f6d0a9819bd499cf..7027870855abf516a63f8c8e3935cce06623e4a0 100644 --- a/app/models/clusters/agents/authorizations/user_access/group_authorization.rb +++ b/app/models/clusters/agents/authorizations/user_access/group_authorization.rb @@ -22,6 +22,10 @@ class GroupAuthorization < ApplicationRecord .order('id, access_level DESC') } + scope :for_project, ->(project) { + where('all_groups_with_membership.traversal_ids @> ARRAY[?]', project.namespace_id) + } + validates :config, json_schema: { filename: 'clusters_agents_authorizations_user_access_config' } def config_project @@ -44,7 +48,9 @@ def all_groups_with_membership_cte def all_groups_with_membership ::Group.joins('INNER JOIN groups_with_direct_membership ON ' \ 'namespaces.traversal_ids @> ARRAY[groups_with_direct_membership.id]') - .select('namespaces.id AS id, groups_with_direct_membership.access_level AS access_level') + .select('namespaces.id AS id, ' \ + 'namespaces.traversal_ids AS traversal_ids, ' \ + 'groups_with_direct_membership.access_level AS access_level') end def groups_with_direct_membership_cte(user) diff --git a/app/models/clusters/agents/authorizations/user_access/project_authorization.rb b/app/models/clusters/agents/authorizations/user_access/project_authorization.rb index 3e2e6f6d35a6b4689ec6a9fcd9f5ceaa2e9e7219..476666e3ad8ab6b7456485df37d018fd24503c0c 100644 --- a/app/models/clusters/agents/authorizations/user_access/project_authorization.rb +++ b/app/models/clusters/agents/authorizations/user_access/project_authorization.rb @@ -19,6 +19,8 @@ class ProjectAuthorization < ApplicationRecord .select('agent_user_access_project_authorizations.*, project_authorizations.access_level AS access_level') } + scope :for_project, ->(project) { where(project: project) } + validates :config, json_schema: { filename: 'clusters_agents_authorizations_user_access_config' } def config_project diff --git a/app/policies/clusters/agent_policy.rb b/app/policies/clusters/agent_policy.rb index afacf782a76abe8279c9b6182433e2153f30ca02..ecd83cceb8bc54b33f5bab427ba34a94c31232f0 100644 --- a/app/policies/clusters/agent_policy.rb +++ b/app/policies/clusters/agent_policy.rb @@ -12,7 +12,11 @@ class AgentPolicy < BasePolicy @subject.ci_access_authorized_for?(@user) end - rule { ci_access_authorized_agent }.policy do + condition(:user_access_authorized_agent, score: 10) do + @subject.user_access_authorized_for?(@user) + end + + rule { ci_access_authorized_agent | user_access_authorized_agent }.policy do enable :read_cluster_agent end end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 9babc0dfcbd563d058d8b760d7e6d64c8fa4a1ae..8c6948119002f6508fd011d0726ce48f1e63aa26 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7680,6 +7680,29 @@ The edge type for [`ClusterAgentAuthorizationCiAccess`](#clusteragentauthorizati | <a id="clusteragentauthorizationciaccessedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="clusteragentauthorizationciaccessedgenode"></a>`node` | [`ClusterAgentAuthorizationCiAccess`](#clusteragentauthorizationciaccess) | The item at the end of the edge. | +#### `ClusterAgentAuthorizationUserAccessConnection` + +The connection type for [`ClusterAgentAuthorizationUserAccess`](#clusteragentauthorizationuseraccess). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="clusteragentauthorizationuseraccessconnectionedges"></a>`edges` | [`[ClusterAgentAuthorizationUserAccessEdge]`](#clusteragentauthorizationuseraccessedge) | A list of edges. | +| <a id="clusteragentauthorizationuseraccessconnectionnodes"></a>`nodes` | [`[ClusterAgentAuthorizationUserAccess]`](#clusteragentauthorizationuseraccess) | A list of nodes. | +| <a id="clusteragentauthorizationuseraccessconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `ClusterAgentAuthorizationUserAccessEdge` + +The edge type for [`ClusterAgentAuthorizationUserAccess`](#clusteragentauthorizationuseraccess). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="clusteragentauthorizationuseraccessedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | +| <a id="clusteragentauthorizationuseraccessedgenode"></a>`node` | [`ClusterAgentAuthorizationUserAccess`](#clusteragentauthorizationuseraccess) | The item at the end of the edge. | + #### `ClusterAgentConnection` The connection type for [`ClusterAgent`](#clusteragent). @@ -12392,6 +12415,15 @@ GitLab CI/CD configuration template. | <a id="clusteragentauthorizationciaccessagent"></a>`agent` | [`ClusterAgent`](#clusteragent) | Authorized cluster agent. | | <a id="clusteragentauthorizationciaccessconfig"></a>`config` | [`JSON`](#json) | Configuration for the authorized project. | +### `ClusterAgentAuthorizationUserAccess` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="clusteragentauthorizationuseraccessagent"></a>`agent` | [`ClusterAgent`](#clusteragent) | Authorized cluster agent. | +| <a id="clusteragentauthorizationuseraccessconfig"></a>`config` | [`JSON`](#json) | Configuration for the authorized project. | + ### `ClusterAgentToken` #### Fields @@ -18692,6 +18724,7 @@ Represents a product analytics dashboard visualization. | <a id="projectterraformstates"></a>`terraformStates` | [`TerraformStateConnection`](#terraformstateconnection) | Terraform states associated with the project. (see [Connections](#connections)) | | <a id="projecttimelogcategories"></a>`timelogCategories` **{warning-solid}** | [`TimeTrackingTimelogCategoryConnection`](#timetrackingtimelogcategoryconnection) | **Introduced** in 15.3. This feature is an Experiment. It can be changed or removed at any time. Timelog categories for the project. | | <a id="projecttopics"></a>`topics` | [`[String!]`](#string) | List of project topics. | +| <a id="projectuseraccessauthorizedagents"></a>`userAccessAuthorizedAgents` | [`ClusterAgentAuthorizationUserAccessConnection`](#clusteragentauthorizationuseraccessconnection) | Authorized cluster agents for the project through user_access keyword. (see [Connections](#connections)) | | <a id="projectuserpermissions"></a>`userPermissions` | [`ProjectPermissions!`](#projectpermissions) | Permissions for the current user on the resource. | | <a id="projectvisibility"></a>`visibility` | [`String`](#string) | Visibility of the project. | | <a id="projectvulnerabilityimages"></a>`vulnerabilityImages` | [`VulnerabilityContainerImageConnection`](#vulnerabilitycontainerimageconnection) | Container images reported on the project vulnerabilities. (see [Connections](#connections)) | diff --git a/spec/finders/clusters/agents/authorizations/user_access/finder_spec.rb b/spec/finders/clusters/agents/authorizations/user_access/finder_spec.rb index 84112502d80e00385fdbca570733912d606f012d..7e6897a723d70e173cfda028196f76eb678bfa03 100644 --- a/spec/finders/clusters/agents/authorizations/user_access/finder_spec.rb +++ b/spec/finders/clusters/agents/authorizations/user_access/finder_spec.rb @@ -22,12 +22,12 @@ end context 'with project authorizations' do - let!(:authorization) do + let!(:authorization_1) do create(:agent_user_access_project_authorization, agent: agent, project: deployment_project) end it 'returns authorization' do - is_expected.to eq([authorization]) + is_expected.to contain_exactly(authorization_1) expect(subject.first.access_level).to eq(Gitlab::Access::DEVELOPER) end @@ -36,7 +36,7 @@ let(:user) { deployment_maintainer } it 'returns authorization' do - is_expected.to eq([authorization]) + is_expected.to contain_exactly(authorization_1) expect(subject.first.access_level).to eq(Gitlab::Access::MAINTAINER) end @@ -52,27 +52,53 @@ context 'with multiple authorizations' do let_it_be(:agent_2) { create(:cluster_agent, project: agent_configuration_project) } + let_it_be(:agent_3) { create(:cluster_agent, project: agent_configuration_project) } + let_it_be(:deployment_project_2) { create(:project, namespace: organization) } let_it_be(:authorization_2) do create(:agent_user_access_project_authorization, agent: agent_2, project: deployment_project) end + let_it_be(:authorization_3) do + create(:agent_user_access_project_authorization, agent: agent_3, project: deployment_project_2) + end + + before_all do + deployment_project_2.add_developer(deployment_developer) + end + it 'returns authorizations' do - is_expected.to contain_exactly(authorization, authorization_2) + is_expected.to contain_exactly(authorization_1, authorization_2, authorization_3) end context 'with specific agent' do let(:params) { { agent: agent_2 } } it 'returns authorization' do - is_expected.to eq([authorization_2]) + is_expected.to contain_exactly(authorization_2) + end + end + + context 'with specific project' do + let(:params) { { project: deployment_project_2 } } + + it 'returns authorization' do + is_expected.to contain_exactly(authorization_3) + end + end + + context 'with limit' do + let(:params) { { limit: 1 } } + + it 'returns authorization' do + expect(subject.count).to eq(1) end end end end context 'with group authorizations' do - let!(:authorization) do + let!(:authorization_1) do create(:agent_user_access_group_authorization, agent: agent, group: organization) end @@ -83,7 +109,7 @@ end it 'returns authorization' do - is_expected.to eq([authorization]) + is_expected.to contain_exactly(authorization_1) expect(subject.first.access_level).to eq(Gitlab::Access::DEVELOPER) end @@ -92,7 +118,7 @@ let(:user) { deployment_maintainer } it 'returns authorization' do - is_expected.to eq([authorization]) + is_expected.to contain_exactly(authorization_1) expect(subject.first.access_level).to eq(Gitlab::Access::MAINTAINER) end @@ -113,8 +139,10 @@ create(:agent_user_access_group_authorization, agent: agent_2, group: organization) end + let_it_be(:authorization_3) { create(:agent_user_access_group_authorization) } + it 'returns authorizations' do - is_expected.to contain_exactly(authorization, authorization_2) + is_expected.to contain_exactly(authorization_1, authorization_2) end context 'with specific agent' do @@ -124,20 +152,46 @@ is_expected.to eq([authorization_2]) end end + + context 'with specific project' do + let(:params) { { project: deployment_project } } + + it 'returns authorization' do + is_expected.to contain_exactly(authorization_1, authorization_2) + end + end + + context 'with limit' do + let(:params) { { limit: 1 } } + + it 'returns authorization' do + expect(subject.count).to eq(1) + end + end end context 'when sub-group is authorized' do - let_it_be(:subgroup) { create(:group, parent: organization) } + let_it_be(:subgroup_1) { create(:group, parent: organization) } + let_it_be(:subgroup_2) { create(:group, parent: organization) } + let_it_be(:deployment_project_1) { create(:project, group: subgroup_1) } + let_it_be(:deployment_project_2) { create(:project, group: subgroup_2) } - let!(:authorization) do - create(:agent_user_access_group_authorization, agent: agent, group: subgroup) - end + let!(:authorization_1) { create(:agent_user_access_group_authorization, agent: agent, group: subgroup_1) } + let!(:authorization_2) { create(:agent_user_access_group_authorization, agent: agent, group: subgroup_2) } it 'returns authorization' do - is_expected.to eq([authorization]) + is_expected.to contain_exactly(authorization_1, authorization_2) expect(subject.first.access_level).to eq(Gitlab::Access::DEVELOPER) end + + context 'with specific deployment project' do + let(:params) { { project: deployment_project_1 } } + + it 'returns only the authorization connected to the parent group' do + is_expected.to contain_exactly(authorization_1) + end + end end end end diff --git a/spec/graphql/resolvers/clusters/agents/authorizations/user_access_resolver_spec.rb b/spec/graphql/resolvers/clusters/agents/authorizations/user_access_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b7e2fef78eb935e1002aa0bb589e5d64412c7a00 --- /dev/null +++ b/spec/graphql/resolvers/clusters/agents/authorizations/user_access_resolver_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Clusters::Agents::Authorizations::UserAccessResolver, + feature_category: :deployment_management do + include GraphqlHelpers + + it { expect(described_class.type).to eq(Types::Clusters::Agents::Authorizations::UserAccessType) } + it { expect(described_class.null).to be_truthy } + + describe '#resolve' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user, maintainer_projects: [project]) } + + let(:ctx) { { current_user: user } } + + subject { resolve(described_class, obj: project, ctx: ctx) } + + it 'calls the finder' do + expect_next_instance_of(::Clusters::Agents::Authorizations::UserAccess::Finder, + user, project: project) do |finder| + expect(finder).to receive(:execute) + end + + subject + end + end +end diff --git a/spec/graphql/types/clusters/agents/authorizations/user_access_type_spec.rb b/spec/graphql/types/clusters/agents/authorizations/user_access_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0e798cd1b18cd0103e3e41563a9f735c04575bd3 --- /dev/null +++ b/spec/graphql/types/clusters/agents/authorizations/user_access_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ClusterAgentAuthorizationUserAccess'], + feature_category: :deployment_management do + let(:fields) { %i[agent config] } + + it { expect(described_class.graphql_name).to eq('ClusterAgentAuthorizationUserAccess') } + it { expect(described_class).to have_graphql_fields(fields) } +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 4cae970de647390bcdfe9a117f856833cd55d95b..5c2e67ca787b4e2fe56bd6f44890b49bdcf5bfd1 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -34,7 +34,7 @@ issue_status_counts terraform_states alert_management_integrations container_repositories container_repositories_count pipeline_analytics squash_read_only sast_ci_configuration - cluster_agent cluster_agents agent_configurations ci_access_authorized_agents + cluster_agent cluster_agents agent_configurations ci_access_authorized_agents user_access_authorized_agents ci_template timelogs merge_commit_template squash_commit_template work_item_types recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb index c2b8c5ea0d0c3033d563feb9c3514415fd56d4e1..10081b955f44ca6bc02e1f7a2d46e6c2cb9a092b 100644 --- a/spec/models/clusters/agent_spec.rb +++ b/spec/models/clusters/agent_spec.rb @@ -236,6 +236,77 @@ end end + describe '#user_access_authorized_for?' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:organization) { create(:group) } + let_it_be(:agent_management_project) { create(:project, group: organization) } + let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) } + let_it_be(:deployment_project) { create(:project, group: organization) } + + let(:user) { create(:user) } + + subject { agent.user_access_authorized_for?(user) } + + it { is_expected.to eq(false) } + + context 'with project-level authorization' do + let!(:authorization) { create(:agent_user_access_project_authorization, agent: agent, project: deployment_project) } + + where(:user_role, :allowed) do + :guest | false + :reporter | false + :developer | true + :maintainer | true + :owner | true + end + + with_them do + before do + deployment_project.add_member(user, user_role) + end + + it { is_expected.to eq(allowed) } + end + + context 'when expose_authorized_cluster_agents feature flag is disabled' do + before do + stub_feature_flags(expose_authorized_cluster_agents: false) + end + + it { is_expected.to eq(false) } + end + end + + context 'with group-level authorization' do + let!(:authorization) { create(:agent_user_access_group_authorization, agent: agent, group: organization) } + + where(:user_role, :allowed) do + :guest | false + :reporter | false + :developer | true + :maintainer | true + :owner | true + end + + with_them do + before do + organization.add_member(user, user_role) + end + + it { is_expected.to eq(allowed) } + end + + context 'when expose_authorized_cluster_agents feature flag is disabled' do + before do + stub_feature_flags(expose_authorized_cluster_agents: false) + end + + it { is_expected.to eq(false) } + end + end + end + describe '#user_access_config' do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project) } diff --git a/spec/policies/clusters/agent_policy_spec.rb b/spec/policies/clusters/agent_policy_spec.rb index 200cb8ae99bc7637380d226a3db9bd9faaf9612d..b3c43647a847156c1a5d9ec2f640f90807cfb50f 100644 --- a/spec/policies/clusters/agent_policy_spec.rb +++ b/spec/policies/clusters/agent_policy_spec.rb @@ -9,6 +9,8 @@ let(:project) { cluster_agent.project } describe 'rules' do + it { expect(policy).to be_disallowed :read_cluster_agent } + context 'when developer' do before do project.add_developer(user) @@ -32,5 +34,13 @@ it { expect(policy).to be_allowed :read_cluster_agent } end + + context 'when agent is user_access authorized for project members' do + before do + allow(cluster_agent).to receive(:user_access_authorized_for?).with(user).and_return(true) + end + + it { expect(policy).to be_allowed :read_cluster_agent } + end end end diff --git a/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b8017171fd1c4c9f1dabee464d0b38d31702c7e1 --- /dev/null +++ b/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project.user_access_authorized_agents', feature_category: :deployment_management do + include GraphqlHelpers + + let_it_be(:organization) { create(:group) } + let_it_be(:agent_management_project) { create(:project, :private, group: organization) } + let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) } + + let_it_be(:deployment_project) { create(:project, :private, group: organization) } + let_it_be(:deployment_developer) { create(:user).tap { |u| deployment_project.add_developer(u) } } + let_it_be(:deployment_reporter) { create(:user).tap { |u| deployment_project.add_reporter(u) } } + + let(:user) { deployment_developer } + + let(:query) do + %( + query { + project(fullPath: "#{deployment_project.full_path}") { + userAccessAuthorizedAgents { + nodes { + agent { + id + name + project { + name + } + } + config + } + } + } + } + ) + end + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + context 'with project authorization' do + let!(:user_access) { create(:agent_user_access_project_authorization, agent: agent, project: deployment_project) } + + it 'returns the authorized agent' do + authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents.count).to eq(1) + + authorized_agent = authorized_agents.first + + expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s) + expect(authorized_agent['agent']['name']).to eq(agent.name) + expect(authorized_agent['config']).to eq({}) + expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources. + end + + context 'when user is developer in the agent management project' do + before do + agent_management_project.add_developer(deployment_developer) + end + + it 'returns the project information as well' do + authorized_agent = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes').first + + expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name) + end + end + + context 'when user is reporter' do + let(:user) { deployment_reporter } + + it 'returns nothing' do + expect(subject['data']['project']['userAccessAuthorizedAgents']).to be_nil + end + end + end + + context 'with group authorization' do + let_it_be(:deployment_group) { create(:group, :private, parent: organization) } + + let!(:user_access) { create(:agent_user_access_group_authorization, agent: agent, group: deployment_group) } + + before_all do + deployment_group.add_developer(deployment_developer) + deployment_group.add_reporter(deployment_reporter) + end + + it 'returns the authorized agent' do + authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents.count).to eq(1) + + authorized_agent = authorized_agents.first + + expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s) + expect(authorized_agent['agent']['name']).to eq(agent.name) + expect(authorized_agent['config']).to eq({}) + expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources. + end + + context 'when user is developer in the agent management project' do + before do + agent_management_project.add_developer(deployment_developer) + end + + it 'returns the project information as well' do + authorized_agent = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes').first + + expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name) + end + end + + context 'when user is reporter' do + let(:user) { deployment_reporter } + + it 'returns nothing' do + expect(subject['data']['project']['userAccessAuthorizedAgents']).to be_nil + end + end + end + + context 'when deployment project is not authorized to user_access to the agent' do + it 'returns empty' do + authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents).to be_empty + end + end +end