diff --git a/app/graphql/mutations/ci/job_token_scope/add_project.rb b/app/graphql/mutations/ci/job_token_scope/add_project.rb index e16c08cb116c4d263d83b99759f92fd9ddbeef20..f31c800d209c3fbbe2f348e8c83ac2f3b21a32a7 100644 --- a/app/graphql/mutations/ci/job_token_scope/add_project.rb +++ b/app/graphql/mutations/ci/job_token_scope/add_project.rb @@ -18,18 +18,23 @@ class AddProject < BaseMutation required: true, description: 'Project to be added to the CI job token scope.' + argument :direction, + ::Types::Ci::JobTokenScope::DirectionEnum, + required: false, + description: 'Direction of access, which defaults to outbound.' + field :ci_job_token_scope, - Types::Ci::JobTokenScopeType, - null: true, - description: "CI job token's scope of access." + Types::Ci::JobTokenScopeType, + null: true, + description: "CI job token's scope of access." - def resolve(project_path:, target_project_path:) + def resolve(project_path:, target_project_path:, direction: :outbound) project = authorized_find!(project_path) target_project = Project.find_by_full_path(target_project_path) result = ::Ci::JobTokenScope::AddProjectService .new(project, current_user) - .execute(target_project) + .execute(target_project, direction: direction) if result.success? { diff --git a/app/graphql/types/ci/job_token_scope/direction_enum.rb b/app/graphql/types/ci/job_token_scope/direction_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..7dc57c2226c0d521a7d9fe1c873ade32f2128cff --- /dev/null +++ b/app/graphql/types/ci/job_token_scope/direction_enum.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module Ci + module JobTokenScope + class DirectionEnum < BaseEnum + graphql_name 'CiJobTokenScopeDirection' + description 'Direction of access.' + + value 'OUTBOUND', + value: :outbound, + description: 'Job token scopes project can access target project in the outbound allowlist.' + + value 'INBOUND', + value: :inbound, + description: 'Target projects in the inbound allowlist can access the scope project ' \ + 'through their job tokens.' + end + end + end +end diff --git a/app/graphql/types/ci/job_token_scope_type.rb b/app/graphql/types/ci/job_token_scope_type.rb index 37c0af944a7b25dd7d0f2c38d51a245158a0a56d..639bbaa22afd32d89da3fe7598d822245b95ec59 100644 --- a/app/graphql/types/ci/job_token_scope_type.rb +++ b/app/graphql/types/ci/job_token_scope_type.rb @@ -11,7 +11,23 @@ class JobTokenScopeType < BaseObject Types::ProjectType.connection_type, null: false, description: 'Allow list of projects that can be accessed by CI Job tokens created by this project.', - method: :all_projects + method: :outbound_projects, + deprecated: { + reason: 'The `projects` attribute is being deprecated. Use `outbound_allowlist`', + milestone: '15.9' + } + + field :outbound_allowlist, + Types::ProjectType.connection_type, + null: false, + description: "Allow list of projects that are accessible using the current project's CI Job tokens.", + method: :outbound_projects + + field :inbound_allowlist, + Types::ProjectType.connection_type, + null: false, + description: "Allow list of projects that can access the current project through its CI Job tokens.", + method: :inbound_projects end end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/models/ci/job_token/allowlist.rb b/app/models/ci/job_token/allowlist.rb index 9e9a0a68ebd438671464c6945bc09b7af8a76845..6b43f11b688434b2461b178953d374b7f70e8a56 100644 --- a/app/models/ci/job_token/allowlist.rb +++ b/app/models/ci/job_token/allowlist.rb @@ -17,8 +17,25 @@ def projects Project.from_union(target_projects, remove_duplicates: false) end + def add!(target_project, user:) + Ci::JobToken::ProjectScopeLink.create!( + source_project: @source_project, + direction: @direction, + target_project: target_project, + added_by: user + ) + end + + def remove!(target_project) + link_for(target_project).destroy + end + private + def link_for(target_project) + source_links.with_target(target_project).take + end + def source_links Ci::JobToken::ProjectScopeLink .with_source(@source_project) diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb index b784f93651a2b16ef8a21e686a99355920f6a6d2..4fb577f58bc6d6dcc23032a2589f17c03ddcd7a3 100644 --- a/app/models/ci/job_token/project_scope_link.rb +++ b/app/models/ci/job_token/project_scope_link.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# The connection between a source project (which defines the job token scope) -# and a target project which is the one allowed to be accessed by the job token. +# The connection between a source project (which the job token scope's allowlist applies too) +# and a target project which is added to the scope's allowlist. module Ci module JobToken @@ -9,6 +9,7 @@ class ProjectScopeLink < Ci::ApplicationRecord self.table_name = 'ci_job_token_project_scope_links' belongs_to :source_project, class_name: 'Project' + # the project added to the scope's allowlist belongs_to :target_project, class_name: 'Project' belongs_to :added_by, class_name: 'User' @@ -19,6 +20,8 @@ class ProjectScopeLink < Ci::ApplicationRecord validates :target_project, presence: true validate :not_self_referential_link + # When outbound the target project is allowed to be accessed by the source job token. + # When inbound the source project is allowed to be accessed by the target job token. enum direction: { outbound: 0, inbound: 1 diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb index e320c0f92d14eb8f46f8dc649efdfc8d64340ec6..93bfa35f5c6337ef605e6ab6766b750311c8b2d9 100644 --- a/app/models/ci/job_token/scope.rb +++ b/app/models/ci/job_token/scope.rb @@ -2,18 +2,17 @@ # This model represents the scope of access for a CI_JOB_TOKEN. # -# A scope is initialized with a project. +# A scope is initialized with a current project. # # Projects can be added to the scope by adding ScopeLinks to # create an allowlist of projects in either access direction (inbound, outbound). # -# Currently, projects in the outbound allowlist can be accessed via the token -# in the source project. +# Projects in the outbound allowlist can be accessed via the current project's job token. # -# TODO(Issue #346298) Projects in the inbound allowlist can use their token to access -# the source project. +# Projects in the inbound allowlist can use their project's job token to +# access the current project. # -# CI_JOB_TOKEN should be considered untrusted without these features enabled. +# CI_JOB_TOKEN should be considered untrusted without a scope enabled. # module Ci @@ -25,34 +24,61 @@ def initialize(current_project) @current_project = current_project end - def allows?(accessed_project) - self_referential?(accessed_project) || outbound_allows?(accessed_project) + def accessible?(accessed_project) + self_referential?(accessed_project) || ( + outbound_accessible?(accessed_project) && + inbound_accessible?(accessed_project) + ) end def outbound_projects outbound_allowlist.projects end - # Deprecated: use outbound_projects, TODO(Issue #346298) remove references to all_project - def all_projects - outbound_projects + def inbound_projects + inbound_allowlist.projects end private - def outbound_allows?(accessed_project) + def outbound_accessible?(accessed_project) # if the setting is disabled any project is considered to be in scope. - return true unless @current_project.ci_outbound_job_token_scope_enabled? + return true unless current_project.ci_outbound_job_token_scope_enabled? outbound_allowlist.includes?(accessed_project) end + def inbound_accessible?(accessed_project) + # if the flag or setting is disabled any project is considered to be in scope. + return true unless Feature.enabled?(:ci_inbound_job_token_scope, current_project) + return true unless current_project.ci_inbound_job_token_scope_enabled? + + inbound_linked_as_accessible?(accessed_project) + end + + # We don't check the inbound allowlist here. That is because + # the access check starts from the current project but the inbound + # allowlist contains projects that can access the current project. + def inbound_linked_as_accessible?(accessed_project) + inbound_accessible_projects(accessed_project).includes?(current_project) + end + + def inbound_accessible_projects(accessed_project) + Ci::JobToken::Allowlist.new(accessed_project, direction: :inbound) + end + + # User created list of projects allowed to access the current project + def inbound_allowlist + Ci::JobToken::Allowlist.new(current_project, direction: :inbound) + end + + # User created list of projects that can be accessed from the current project def outbound_allowlist - Ci::JobToken::Allowlist.new(@current_project, direction: :outbound) + Ci::JobToken::Allowlist.new(current_project, direction: :outbound) end def self_referential?(accessed_project) - @current_project.id == accessed_project.id + current_project.id == accessed_project.id end end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 34c5be7d972beb4419f5b1ee5fe57afdbf908ed2..eb9d13cc9a3f23d527468bedfd5354c05b541438 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -121,7 +121,7 @@ class ProjectPolicy < BasePolicy desc "If user is authenticated via CI job token then the target project should be in scope" condition(:project_allowed_for_job_token) do - !@user&.from_ci_job_token? || @user.ci_job_token_scope.allows?(project) + !@user&.from_ci_job_token? || @user.ci_job_token_scope.accessible?(project) end with_scope :subject diff --git a/app/services/ci/job_token_scope/add_project_service.rb b/app/services/ci/job_token_scope/add_project_service.rb index d03ae434b69a750406b25abdc90e913da0d6cd06..12a369980431813054c6aee851e2a08037255372 100644 --- a/app/services/ci/job_token_scope/add_project_service.rb +++ b/app/services/ci/job_token_scope/add_project_service.rb @@ -5,10 +5,14 @@ module JobTokenScope class AddProjectService < ::BaseService include EditScopeValidations - def execute(target_project) + def execute(target_project, direction: :outbound) + direction = :outbound unless Feature.enabled?(:ci_inbound_job_token_scope) + validate_edit!(project, target_project, current_user) - link = add_project!(target_project) + link = allowlist(direction) + .add!(target_project, user: current_user) + ServiceResponse.success(payload: { project_link: link }) rescue ActiveRecord::RecordNotUnique @@ -19,12 +23,10 @@ def execute(target_project) ServiceResponse.error(message: e.message) end - def add_project!(target_project) - ::Ci::JobToken::ProjectScopeLink.create!( - source_project: project, - target_project: target_project, - added_by: current_user - ) + private + + def allowlist(direction) + Ci::JobToken::Allowlist.new(project, direction: direction) end end end diff --git a/app/services/ci/job_token_scope/remove_project_service.rb b/app/services/ci/job_token_scope/remove_project_service.rb index 15644e529d99878b7411a4f775c74a96e2081d09..322330af8f1b25e8fcd7f07ec9052ce1fc48da4c 100644 --- a/app/services/ci/job_token_scope/remove_project_service.rb +++ b/app/services/ci/job_token_scope/remove_project_service.rb @@ -5,20 +5,18 @@ module JobTokenScope class RemoveProjectService < ::BaseService include EditScopeValidations - def execute(target_project) + def execute(target_project, direction: :outbound) validate_edit!(project, target_project, current_user) if project == target_project return ServiceResponse.error(message: "Source project cannot be removed from the job token scope") end - link = ::Ci::JobToken::ProjectScopeLink.for_source_and_target(project, target_project) - - unless link + unless allowlist(direction).includes?(target_project) return ServiceResponse.error(message: "Target project is not in the job token scope") end - if link.destroy + if allowlist(direction).remove!(target_project) ServiceResponse.success else ServiceResponse.error(message: link.errors.full_messages.to_sentence, payload: { project_link: link }) @@ -26,6 +24,12 @@ def execute(target_project) rescue EditScopeValidations::ValidationError => e ServiceResponse.error(message: e.message) end + + private + + def allowlist(direction) + Ci::JobToken::Allowlist.new(project, direction: direction) + end end end end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d01e8e3e7c3bd1b2f6371f16d521847392990fd6..c680882942ea0b2c6fd61861802ac27d961e9e59 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1183,6 +1183,7 @@ Input type: `CiJobTokenScopeAddProjectInput` | Name | Type | Description | | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `direction` | [`CiJobTokenScopeDirection`](#cijobtokenscopedirection) | Direction of access, which defaults to outbound. | | `projectPath` | [`ID!`](#id) | Project that the CI job token scope belongs to. | | `targetProjectPath` | [`ID!`](#id) | Project to be added to the CI job token scope. | @@ -11299,7 +11300,9 @@ CI/CD variables for a GitLab instance. | Name | Type | Description | | ---- | ---- | ----------- | -| `projects` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that can be accessed by CI Job tokens created by this project. (see [Connections](#connections)) | +| `inboundAllowlist` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that can access the current project through its CI Job tokens. (see [Connections](#connections)) | +| `outboundAllowlist` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that are accessible using the current project's CI Job tokens. (see [Connections](#connections)) | +| `projects` **{warning-solid}** | [`ProjectConnection!`](#projectconnection) | **Deprecated** in 15.9. The `projects` attribute is being deprecated. Use `outbound_allowlist`. | ### `CiJobsDurationStatistics` @@ -21706,6 +21709,15 @@ Deploy freeze period status. | `SUCCESS` | A job that is success. | | `WAITING_FOR_RESOURCE` | A job that is waiting for resource. | +### `CiJobTokenScopeDirection` + +Direction of access. + +| Value | Description | +| ----- | ----------- | +| `INBOUND` | Target projects in the inbound allowlist can access the scope project through their job tokens. | +| `OUTBOUND` | Job token scopes project can access target project in the outbound allowlist. | + ### `CiRunnerAccessLevel` | Value | Description | diff --git a/ee/spec/requests/api/ci/runner_spec.rb b/ee/spec/requests/api/ci/runner_spec.rb index 388bf299e7d8b595a9997a6edc6598491e3013d1..7f54f5a4d43dc74e67325643ee2268f97da868b5 100644 --- a/ee/spec/requests/api/ci/runner_spec.rb +++ b/ee/spec/requests/api/ci/runner_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe API::Ci::Runner, feature_category: :runner do + include Ci::JobTokenScopeHelpers + let_it_be_with_reload(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -127,10 +129,10 @@ def request_job(token = runner.token, **params) end describe 'GET api/v4/jobs/:id/artifacts' do - let_it_be(:job) { create(:ci_build, :success, ref: ref, pipeline: pipeline, user: user) } + let_it_be(:job) { create(:ci_build, :success, ref: ref, pipeline: pipeline, user: user, project: project) } before_all do - create(:ci_job_artifact, :archive, job: job) + create(:ci_job_artifact, :archive, job: job, project: project) end shared_examples 'successful artifact download' do @@ -176,6 +178,7 @@ def request_job(token = runner.token, **params) before_all do downstream_project.add_developer(user) downstream_project.add_developer(downstream_project_dev) + make_project_fully_accessible(downstream_project, project) end before do diff --git a/ee/spec/requests/api/ci/triggers_spec.rb b/ee/spec/requests/api/ci/triggers_spec.rb index d7c5b21bf1771e42081303fbd94ac599281ad049..39154cf8745bc1d446f33e5aa312b777b8b0104a 100644 --- a/ee/spec/requests/api/ci/triggers_spec.rb +++ b/ee/spec/requests/api/ci/triggers_spec.rb @@ -19,7 +19,7 @@ end before do - allow_next(Ci::JobToken::Scope).to receive(:allows?).with(project).and_return(true) + allow_next(Ci::JobToken::Scope).to receive(:accessible?).with(project).and_return(true) end context 'without user' do @@ -80,7 +80,7 @@ context 'when project is not in the job token scope' do before do allow_next(Ci::JobToken::Scope) - .to receive(:allows?) + .to receive(:accessible?) .with(project) .and_return(false) end diff --git a/ee/spec/requests/api/internal/app_sec/dast/site_validations_spec.rb b/ee/spec/requests/api/internal/app_sec/dast/site_validations_spec.rb index be43c7a8761e31eb63d0aeddb8cb7615c726b37f..8377a53eec1102003e4ceaa60f5e3aeb5aabc794 100644 --- a/ee/spec/requests/api/internal/app_sec/dast/site_validations_spec.rb +++ b/ee/spec/requests/api/internal/app_sec/dast/site_validations_spec.rb @@ -4,6 +4,7 @@ RSpec.describe API::Internal::AppSec::Dast::SiteValidations, feature_category: :dynamic_application_security_testing do include AfterNextHelpers + include Ci::JobTokenScopeHelpers let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user, developer_projects: [project]) } @@ -69,10 +70,7 @@ let_it_be(:job) { create(:ci_build, :running, user: developer) } before do - create(:ci_job_token_project_scope_link, - source_project: job.project, - target_project: project, - added_by: developer) + make_project_fully_accessible(job.project, project) end it 'returns 400', :aggregate_failures do diff --git a/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb index 727db7e23611b783c50f9c8758c95413ab63d185..44147987ebb4b0b47038161eb1380a7aeea2bae0 100644 --- a/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb +++ b/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Mutations::Ci::JobTokenScope::AddProject do +RSpec.describe Mutations::Ci::JobTokenScope::AddProject, feature_category: :continuous_integration do let(:mutation) do described_class.new(object: nil, context: { current_user: current_user }, field: nil) end @@ -14,9 +14,10 @@ let_it_be(:target_project) { create(:project) } let(:target_project_path) { target_project.full_path } + let(:mutation_args) { { project_path: project.full_path, target_project_path: target_project_path } } subject do - mutation.resolve(project_path: project.full_path, target_project_path: target_project_path) + mutation.resolve(**mutation_args) end context 'when user is not logged in' do @@ -42,18 +43,45 @@ target_project.add_guest(current_user) end - it 'adds target project to the job token scope' do + it 'adds target project to the outbound job token scope by default' do expect do expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty) - end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1) + end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1) + end + + context 'when mutation uses the direction argument' do + let(:mutation_args) { super().merge!(direction: direction) } + + context 'when targeting the outbound allowlist' do + let(:direction) { :outbound } + + it 'adds the target project' do + expect do + expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty) + end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1) + end + end + + context 'when targeting the inbound allowlist' do + let(:direction) { :inbound } + + it 'adds the target project' do + expect do + expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty) + end.to change { Ci::JobToken::ProjectScopeLink.inbound.count }.by(1) + end + end end context 'when the service returns an error' do let(:service) { double(:service) } it 'returns an error response' do - expect(::Ci::JobTokenScope::AddProjectService).to receive(:new).with(project, current_user).and_return(service) - expect(service).to receive(:execute).with(target_project).and_return(ServiceResponse.error(message: 'The error message')) + expect(::Ci::JobTokenScope::AddProjectService).to receive(:new).with( + project, + current_user + ).and_return(service) + expect(service).to receive(:execute).with(target_project, direction: :outbound).and_return(ServiceResponse.error(message: 'The error message')) expect(subject.fetch(:ci_job_token_scope)).to be_nil expect(subject.fetch(:errors)).to include("The error message") diff --git a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb index 59ece15b745cb136cd3ccabcdf0636d737c5f090..92f4d3dd8e802103c25e360efefb0042807523e0 100644 --- a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb @@ -23,18 +23,18 @@ it 'returns the same project in the allow list of projects for the Ci Job Token when scope is not enabled' do allow(project).to receive(:ci_outbound_job_token_scope_enabled?).and_return(false) - expect(resolve_scope.all_projects).to contain_exactly(project) + expect(resolve_scope.outbound_projects).to contain_exactly(project) end it 'returns the same project in the allow list of projects for the Ci Job Token' do - expect(resolve_scope.all_projects).to contain_exactly(project) + expect(resolve_scope.outbound_projects).to contain_exactly(project) end context 'when another projects gets added to the allow list' do let!(:link) { create(:ci_job_token_project_scope_link, source_project: project) } it 'returns both projects' do - expect(resolve_scope.all_projects).to contain_exactly(project, link.target_project) + expect(resolve_scope.outbound_projects).to contain_exactly(project, link.target_project) end end @@ -44,7 +44,7 @@ end it 'resolves projects' do - expect(resolve_scope.all_projects).to contain_exactly(project) + expect(resolve_scope.outbound_projects).to contain_exactly(project) end end end diff --git a/spec/graphql/types/ci/job_token_scope_type_spec.rb b/spec/graphql/types/ci/job_token_scope_type_spec.rb index 569b59d6c7090b724f2e4606ede6b76334924150..24597929b5e5db9a6ee23ed73fe665aaa4b30bff 100644 --- a/spec/graphql/types/ci/job_token_scope_type_spec.rb +++ b/spec/graphql/types/ci/job_token_scope_type_spec.rb @@ -2,17 +2,24 @@ require 'spec_helper' -RSpec.describe GitlabSchema.types['CiJobTokenScopeType'] do +RSpec.describe GitlabSchema.types['CiJobTokenScopeType'], feature_category: :continuous_integration do specify { expect(described_class.graphql_name).to eq('CiJobTokenScopeType') } it 'has the correct fields' do - expected_fields = [:projects] + expected_fields = [:projects, :inboundAllowlist, :outboundAllowlist] expect(described_class).to have_graphql_fields(*expected_fields) end describe 'query' do - let(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) } + let(:project) do + create( + :project, + ci_outbound_job_token_scope_enabled: true, + ci_inbound_job_token_scope_enabled: true + ).tap(&:save!) + end + let_it_be(:current_user) { create(:user) } let(:query) do @@ -25,6 +32,16 @@ path } } + inboundAllowlist { + nodes { + path + } + } + outboundAllowlist { + nodes { + path + } + } } } } @@ -33,30 +50,73 @@ subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json } - let(:projects_field) { subject.dig('data', 'project', 'ciJobTokenScope', 'projects', 'nodes') } - let(:returned_project_paths) { projects_field.map { |project| project['path'] } } + let(:scope_field) { subject.dig('data', 'project', 'ciJobTokenScope') } + let(:errors_field) { subject['errors'] } + let(:projects_field) { scope_field&.dig('projects', 'nodes') } + let(:outbound_allowlist_field) { scope_field&.dig('outboundAllowlist', 'nodes') } + let(:inbound_allowlist_field) { scope_field&.dig('inboundAllowlist', 'nodes') } + let(:returned_project_paths) { projects_field.map { |p| p['path'] } } + let(:returned_outbound_paths) { outbound_allowlist_field.map { |p| p['path'] } } + let(:returned_inbound_paths) { inbound_allowlist_field.map { |p| p['path'] } } + + context 'without access to scope' do + before do + project.add_member(current_user, :developer) + end + + it 'returns no projects' do + expect(projects_field).to be_nil + expect(outbound_allowlist_field).to be_nil + expect(inbound_allowlist_field).to be_nil + expect(errors_field.first['message']).to include "don't have permission" + end + end context 'with access to scope' do before do project.add_member(current_user, :maintainer) end - context 'when multiple projects in the allow list' do - let!(:link) { create(:ci_job_token_project_scope_link, source_project: project) } + context 'when multiple projects in the allow lists' do + include Ci::JobTokenScopeHelpers + let!(:outbound_allowlist_project) { create_project_in_allowlist(project, direction: :outbound) } + let!(:inbound_allowlist_project) { create_project_in_allowlist(project, direction: :inbound) } + let!(:both_allowlists_project) { create_project_in_both_allowlists(project) } context 'when linked projects are readable' do before do - link.target_project.add_member(current_user, :developer) + outbound_allowlist_project.add_member(current_user, :developer) + inbound_allowlist_project.add_member(current_user, :developer) + both_allowlists_project.add_member(current_user, :developer) end - it 'returns readable projects in scope' do - expect(returned_project_paths).to contain_exactly(project.path, link.target_project.path) + shared_examples 'returns projects' do + it 'returns readable projects in scope' do + outbound_paths = [project.path, outbound_allowlist_project.path, both_allowlists_project.path] + inbound_paths = [project.path, inbound_allowlist_project.path, both_allowlists_project.path] + + expect(returned_project_paths).to contain_exactly(*outbound_paths) + expect(returned_outbound_paths).to contain_exactly(*outbound_paths) + expect(returned_inbound_paths).to contain_exactly(*inbound_paths) + end + end + + it_behaves_like 'returns projects' + + context 'when job token scope is disabled' do + before do + project.ci_cd_settings.update!(job_token_scope_enabled: false) + end + + it_behaves_like 'returns projects' end end - context 'when linked project is not readable' do + context 'when linked projects are not readable' do it 'returns readable projects in scope' do expect(returned_project_paths).to contain_exactly(project.path) + expect(returned_outbound_paths).to contain_exactly(project.path) + expect(returned_inbound_paths).to contain_exactly(project.path) end end @@ -71,6 +131,7 @@ it 'returns readable projects in scope' do expect(returned_project_paths).to contain_exactly(project.path) + expect(returned_outbound_paths).to contain_exactly(project.path) end end end diff --git a/spec/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb index 45083d64393a7741d2ec8fc6952b12f602952c88..57b76630e1015474d72d47e444dbf3683ad19cd4 100644 --- a/spec/models/ci/job_token/allowlist_spec.rb +++ b/spec/models/ci/job_token/allowlist_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integration do + include Ci::JobTokenScopeHelpers using RSpec::Parameterized::TableSyntax let_it_be(:source_project) { create(:project) } @@ -24,11 +25,11 @@ end context 'when projects are added to the scope' do - include_context 'with scoped projects' + include_context 'with a project in each allowlist' where(:direction, :additional_project) do - :outbound | ref(:outbound_scoped_project) - :inbound | ref(:inbound_scoped_project) + :outbound | ref(:outbound_allowlist_project) + :inbound | ref(:inbound_allowlist_project) end with_them do @@ -57,16 +58,16 @@ end end - context 'with scoped projects' do - include_context 'with scoped projects' + context 'with a project in each allowlist' do + include_context 'with a project in each allowlist' where(:includes_project, :direction, :result) do ref(:source_project) | :outbound | false ref(:source_project) | :inbound | false - ref(:inbound_scoped_project) | :outbound | false - ref(:inbound_scoped_project) | :inbound | true - ref(:outbound_scoped_project) | :outbound | true - ref(:outbound_scoped_project) | :inbound | false + ref(:inbound_allowlist_project) | :outbound | false + ref(:inbound_allowlist_project) | :inbound | true + ref(:outbound_allowlist_project) | :outbound | true + ref(:outbound_allowlist_project) | :inbound | false ref(:unscoped_project1) | :outbound | false ref(:unscoped_project1) | :inbound | false ref(:unscoped_project2) | :outbound | false @@ -78,4 +79,37 @@ end end end + + describe '#remove!' do + subject { allowlist.remove!(target_project) } + + let!(:target_project) { create_project_in_allowlist(source_project, direction: direction) } + + context 'when a link does not exist' do + let(:target_project) { create(:project) } + + it 'returns a ServiceResponse error explaining that the link does not exist' do + expect(subject).to be_error + expect(subject.errors).to include('Target project is not in the job token scope') + end + end + + context 'for an inbound link' do + let(:direction) { :inbound } + + it 'successfully destroys the link' do + expect { subject }.to change { Ci::JobToken::ProjectScopeLink.count }.by(-1) + expect(allowlist.projects).not_to include(target_project) + end + end + + context 'when an outbound direction' do + let(:direction) { :outbound } + + it 'successfully destroys the outbound link' do + expect { subject }.to change { Ci::JobToken::ProjectScopeLink.count }.by(-1) + expect(allowlist.projects).not_to include(target_project) + end + end + end end diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb index 91491733c444bad97048a9a29b30046900d591f9..30ae8bc6d8842f41482e2c679720bc15133a3a12 100644 --- a/spec/models/ci/job_token/project_scope_link_spec.rb +++ b/spec/models/ci/job_token/project_scope_link_spec.rb @@ -18,11 +18,12 @@ describe 'unique index' do let!(:link) { create(:ci_job_token_project_scope_link) } - it 'raises an error' do + it 'raises an error, when not unique' do expect do create(:ci_job_token_project_scope_link, source_project: link.source_project, - target_project: link.target_project) + target_project: link.target_project, + direction: link.direction) end.to raise_error(ActiveRecord::RecordNotUnique) end end diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb index 37c5697350658bbeccab77bb81f37fcaff768550..da632622f1bccdf0104f799b78025def09652dc6 100644 --- a/spec/models/ci/job_token/scope_spec.rb +++ b/spec/models/ci/job_token/scope_spec.rb @@ -2,78 +2,144 @@ require 'spec_helper' -RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration do - let_it_be(:source_project) { create(:project, ci_outbound_job_token_scope_enabled: true) } +RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, factory_default: :keep do + include Ci::JobTokenScopeHelpers + using RSpec::Parameterized::TableSyntax + + let_it_be(:project) { create_default(:project) } + let_it_be(:user) { create_default(:user) } + let_it_be(:namespace) { create_default(:namespace) } + + let_it_be(:source_project) do + create(:project, + ci_outbound_job_token_scope_enabled: true, + ci_inbound_job_token_scope_enabled: true + ) + end + + let(:current_project) { source_project } - let(:scope) { described_class.new(source_project) } + let(:scope) { described_class.new(current_project) } - describe '#all_projects' do - subject(:all_projects) { scope.all_projects } + describe '#outbound_projects' do + subject { scope.outbound_projects } context 'when no projects are added to the scope' do it 'returns the project defining the scope' do - expect(all_projects).to contain_exactly(source_project) + expect(subject).to contain_exactly(current_project) end end context 'when projects are added to the scope' do - include_context 'with scoped projects' + include_context 'with accessible and inaccessible projects' it 'returns all projects that can be accessed from a given scope' do - expect(subject).to contain_exactly(source_project, outbound_scoped_project) + expect(subject).to contain_exactly(current_project, outbound_allowlist_project, fully_accessible_project) end end end - describe '#allows?' do - subject { scope.allows?(includes_project) } + describe '#inbound_projects' do + subject { scope.inbound_projects } - context 'without scoped projects' do - context 'when self referential' do - let(:includes_project) { source_project } + context 'when no projects are added to the scope' do + it 'returns the project defining the scope' do + expect(subject).to contain_exactly(current_project) + end + end - it { is_expected.to be_truthy } + context 'when projects are added to the scope' do + include_context 'with accessible and inaccessible projects' + + it 'returns all projects that can be accessed from a given scope' do + expect(subject).to contain_exactly(current_project, inbound_allowlist_project) end end + end - context 'with scoped projects' do - include_context 'with scoped projects' + RSpec.shared_examples 'enforces outbound scope only' do + include_context 'with accessible and inaccessible projects' + + where(:accessed_project, :result) do + ref(:current_project) | true + ref(:inbound_allowlist_project) | false + ref(:unscoped_project1) | false + ref(:unscoped_project2) | false + ref(:outbound_allowlist_project) | true + ref(:inbound_accessible_project) | false + ref(:fully_accessible_project) | true + end - context 'when project is in outbound scope' do - let(:includes_project) { outbound_scoped_project } + with_them do + it { is_expected.to eq(result) } + end + end - it { is_expected.to be_truthy } - end + describe 'accessible?' do + subject { scope.accessible?(accessed_project) } + + context 'with inbound and outbound scopes enabled' do + context 'when inbound and outbound access setup' do + include_context 'with accessible and inaccessible projects' + + where(:accessed_project, :result) do + ref(:current_project) | true + ref(:inbound_allowlist_project) | false + ref(:unscoped_project1) | false + ref(:unscoped_project2) | false + ref(:outbound_allowlist_project) | false + ref(:inbound_accessible_project) | false + ref(:fully_accessible_project) | true + end - context 'when project is in inbound scope' do - let(:includes_project) { inbound_scoped_project } + with_them do + it 'allows self and projects allowed from both directions' do + is_expected.to eq(result) + end + end + end + end - it { is_expected.to be_falsey } + context 'with inbound scope enabled and outbound scope disabled' do + before do + source_project.ci_inbound_job_token_scope_enabled = true + source_project.ci_outbound_job_token_scope_enabled = false + source_project.save! end - context 'when project is linked to a different project' do - let(:includes_project) { unscoped_project1 } + include_context 'with accessible and inaccessible projects' - it { is_expected.to be_falsey } + where(:accessed_project, :result) do + ref(:current_project) | true + ref(:inbound_allowlist_project) | false + ref(:unscoped_project1) | false + ref(:unscoped_project2) | false + ref(:outbound_allowlist_project) | false + ref(:inbound_accessible_project) | true + ref(:fully_accessible_project) | true end - context 'when project is unlinked to a project' do - let(:includes_project) { unscoped_project2 } - - it { is_expected.to be_falsey } + with_them do + it { is_expected.to eq(result) } end + end - context 'when project scope setting is disabled' do - let(:includes_project) { unscoped_project1 } + context 'with inbound scope disabled and outbound scope enabled' do + before do + source_project.ci_inbound_job_token_scope_enabled = false + source_project.ci_outbound_job_token_scope_enabled = true + source_project.save! + end - before do - source_project.ci_outbound_job_token_scope_enabled = false - end + include_examples 'enforces outbound scope only' + end - it 'considers any project to be part of the scope' do - expect(subject).to be_truthy - end + context 'when inbound scope flag disabled' do + before do + stub_feature_flags(ci_inbound_job_token_scope: false) end + + include_examples 'enforces outbound scope only' end end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index a98f091b9fca723d0a951960bd865eb789b13820..434f7a43665127d441c033948bd9901a062e1809 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -2478,7 +2478,10 @@ def set_access_level(access_level) before do current_user.set_ci_job_token_scope!(job) current_user.external = external_user - scope_project.update!(ci_outbound_job_token_scope_enabled: token_scope_enabled) + scope_project.update!( + ci_outbound_job_token_scope_enabled: token_scope_enabled, + ci_inbound_job_token_scope_enabled: token_scope_enabled + ) end it "enforces the expected permissions" do diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb index a4a38179d1141112afc92d0fc638f196d272c603..ee390773f298d2dc595b1681e159d175083820f6 100644 --- a/spec/requests/api/ci/job_artifacts_spec.rb +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -5,6 +5,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do include HttpBasicAuthHelpers include DependencyProxyHelpers + include Ci::JobTokenScopeHelpers include HttpIOHelpers @@ -312,7 +313,7 @@ def get_artifact_file(artifact_path) context 'normal authentication' do context 'job with artifacts' do context 'when artifacts are stored locally' do - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, project: project) } subject { get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) } @@ -329,11 +330,12 @@ def get_artifact_file(artifact_path) stub_licensed_features(cross_project_pipelines: true) end - it_behaves_like 'downloads artifact' - context 'when job token scope is enabled' do before do - other_job.project.ci_cd_settings.update!(job_token_scope_enabled: true) + other_job.project.ci_cd_settings.update!( + job_token_scope_enabled: true, + inbound_job_token_scope_enabled: true + ) end it 'does not allow downloading artifacts' do @@ -343,7 +345,9 @@ def get_artifact_file(artifact_path) end context 'when project is added to the job token scope' do - let!(:link) { create(:ci_job_token_project_scope_link, source_project: other_job.project, target_project: job.project) } + before do + make_project_fully_accessible(other_job.project, job.project) + end it_behaves_like 'downloads artifact' end diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb index 490716ddbe2fd824faa56cea81c0edd40864513f..55e728b2141deb93ea09aa4c6a99a51d9bddcfbe 100644 --- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb @@ -60,7 +60,7 @@ post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty - end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(false).to(true) + end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1) end context 'when invalid target project is provided' do diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb index 607c6bd85c25e33e6ecdf7ea3a8fedd254bd3574..61d5c56ae8a2c310f39ee8a7c65721ed95ef9026 100644 --- a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb @@ -5,7 +5,13 @@ RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_integration do include GraphqlHelpers - let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) } + let_it_be(:project) do + create(:project, + ci_outbound_job_token_scope_enabled: true, + ci_inbound_job_token_scope_enabled: true + ) + end + let_it_be(:target_project) { create(:project) } let_it_be(:link) do @@ -66,7 +72,7 @@ post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty - end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(true).to(false) + end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(-1) end context 'when invalid target project is provided' do diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index d3adef85f8d509586921a72c4ece35c7a3a2a470..c003ae9cd48d7764568c6c8faa8c1a1bf8ec9261 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -88,7 +88,7 @@ end context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } subject { get api(url, job_token: job.token) } @@ -130,7 +130,7 @@ end context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } subject { get api(url, job_token: job.token) } @@ -229,8 +229,8 @@ get api(package_url, user) end - pipeline = create(:ci_pipeline, user: user) - create(:ci_build, user: user, pipeline: pipeline) + pipeline = create(:ci_pipeline, user: user, project: project) + create(:ci_build, user: user, pipeline: pipeline, project: project) create(:package_build_info, package: package1, pipeline: pipeline) expect do @@ -262,7 +262,7 @@ it_behaves_like 'no destroy url' context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } subject { get api(package_url, job_token: job.token) } @@ -324,7 +324,7 @@ end context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } subject { get api(package_url, job_token: job.token) } @@ -430,7 +430,7 @@ end context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } it 'returns 403 for a user without enough permissions' do project.add_developer(user) diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index 4a7821fcb0a137984d1bfbf9a1d1799b6623a33a..462cc1e3b5db85fcf8a8f09fed83a4679ad26289 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe API::Release::Links, feature_category: :release_orchestration do + include Ci::JobTokenScopeHelpers + let(:project) { create(:project, :repository, :private) } let(:maintainer) { create(:user) } let(:developer) { create(:user) } @@ -51,7 +53,7 @@ end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'returns releases links' do get api("/projects/#{project.id}/releases/v0.1/assets/links", job_token: job.token) @@ -127,7 +129,7 @@ end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'returns releases link' do get api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", job_token: job.token) @@ -241,7 +243,7 @@ end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'creates a new release link' do expect do @@ -385,7 +387,7 @@ end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'updates the release link' do put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}"), params: params.merge(job_token: job.token) @@ -496,7 +498,7 @@ end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'deletes the release link' do expect do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 66337b94c7593993b08645068241be3ca7517e20..02b99eba8ced5bf180bfb607a1430bd697832957 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -7,6 +7,7 @@ include TermsHelper include GitHttpHelpers include WorkhorseHelpers + include Ci::JobTokenScopeHelpers shared_examples 'pulls require Basic HTTP Authentication' do context "when no credentials are provided" do @@ -869,14 +870,15 @@ def attempt_login(include_password) context "when a gitlab ci token is provided" do let(:project) { create(:project, :repository) } - let(:build) { create(:ci_build, :running) } - let(:other_project) { create(:project, :repository) } - - before do - build.update!(project: project) # can't associate it on factory create + let(:build) { create(:ci_build, :running, project: project, user: user) } + let(:other_project) do + create(:project, :repository).tap do |o| + make_project_fully_accessible(project, o) + end end context 'when build created by system is authenticated' do + let(:user) { nil } let(:path) { "#{project.full_path}.git" } let(:env) { { user: 'gitlab-ci-token', password: build.token } } @@ -899,12 +901,7 @@ def pull context 'and build created by' do before do - build.update!(user: user) project.add_reporter(user) - create(:ci_job_token_project_scope_link, - source_project: project, - target_project: other_project, - added_by: user) end shared_examples 'can download code only' do @@ -1474,19 +1471,16 @@ def attempt_login(include_password) context "when a gitlab ci token is provided" do let(:project) { create(:project, :repository) } - let(:build) { create(:ci_build, :running) } - let(:other_project) { create(:project, :repository) } - - before do - build.update!(project: project) # can't associate it on factory create - create(:ci_job_token_project_scope_link, - source_project: project, - target_project: other_project, - added_by: user) + let(:build) { create(:ci_build, :running, project: project, user: user) } + let(:other_project) do + create(:project, :repository).tap do |o| + make_project_fully_accessible(project, o) + end end # legacy behavior that is blocked/deprecated context 'when build created by system is authenticated' do + let(:user) { nil } let(:path) { "#{project.full_path}.git" } let(:env) { { user: 'gitlab-ci-token', password: build.token } } @@ -1505,7 +1499,6 @@ def attempt_login(include_password) context 'and build created by' do before do - build.update!(user: user) project.add_reporter(user) end @@ -1862,13 +1855,9 @@ def attempt_login(include_password) end context 'from CI' do - let(:build) { create(:ci_build, :running) } + let(:build) { create(:ci_build, :running, user: user, project: project) } let(:env) { { user: 'gitlab-ci-token', password: build.token } } - before do - build.update!(user: user, project: project) - end - it_behaves_like 'pulls are allowed' end end diff --git a/spec/services/ci/job_token_scope/add_project_service_spec.rb b/spec/services/ci/job_token_scope/add_project_service_spec.rb index bf7df3a5595fe20f11ea6edf9886d7c5ebffc65e..e6674ee384f5d8fd8a1d6e245c810e8deba14509 100644 --- a/spec/services/ci/job_token_scope/add_project_service_spec.rb +++ b/spec/services/ci/job_token_scope/add_project_service_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Ci::JobTokenScope::AddProjectService do +RSpec.describe Ci::JobTokenScope::AddProjectService, feature_category: :continuous_integration do let(:service) { described_class.new(project, current_user) } let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) } @@ -21,6 +21,8 @@ it_behaves_like 'editable job token scope' do context 'when user has permissions on source and target projects' do + let(:resulting_direction) { result.payload.fetch(:project_link)&.direction } + before do project.add_maintainer(current_user) target_project.add_developer(current_user) @@ -34,6 +36,26 @@ end it_behaves_like 'adds project' + + it 'creates an outbound link by default' do + expect(resulting_direction).to eq('outbound') + end + + context 'when direction is specified' do + subject(:result) { service.execute(target_project, direction: direction) } + + context 'when the direction is outbound' do + let(:direction) { :outbound } + + specify { expect(resulting_direction).to eq('outbound') } + end + + context 'when the direction is inbound' do + let(:direction) { :inbound } + + specify { expect(resulting_direction).to eq('inbound') } + end + end end end diff --git a/spec/support/helpers/ci/job_token_scope_helpers.rb b/spec/support/helpers/ci/job_token_scope_helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..f4c747d2d4bcabeed8df879aff885c5c8d9b029d --- /dev/null +++ b/spec/support/helpers/ci/job_token_scope_helpers.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Ci + module JobTokenScopeHelpers + def create_project_in_allowlist(root_project, direction:) + create(:project).tap do |new_project| + create( + :ci_job_token_project_scope_link, + source_project: root_project, + target_project: new_project, + direction: direction + ) + end + end + + def create_project_in_both_allowlists(root_project) + create_project_in_allowlist(root_project, direction: :outbound).tap do |new_project| + create( + :ci_job_token_project_scope_link, + source_project: root_project, + target_project: new_project, + direction: :inbound + ) + end + end + + def create_inbound_accessible_project(project) + create(:project).tap do |accessible_project| + add_inbound_accessible_linkage(project, accessible_project) + end + end + + def create_inbound_and_outbound_accessible_project(project) + create(:project).tap do |accessible_project| + make_project_fully_accessible(project, accessible_project) + end + end + + def make_project_fully_accessible(project, accessible_project) + add_outbound_accessible_linkage(project, accessible_project) + add_inbound_accessible_linkage(project, accessible_project) + end + + def add_outbound_accessible_linkage(project, accessible_project) + create( + :ci_job_token_project_scope_link, + source_project: project, + target_project: accessible_project, + direction: :outbound + ) + end + + def add_inbound_accessible_linkage(project, accessible_project) + create( + :ci_job_token_project_scope_link, + source_project: accessible_project, + target_project: project, + direction: :inbound + ) + end + end +end diff --git a/spec/support/shared_contexts/models/ci/job_token_scope.rb b/spec/support/shared_contexts/models/ci/job_token_scope.rb index 51f671b139d69e78fe78444a8f73b51b537ccfd0..d0fee23b57c43467b9be0c4b4b3acbdbd233f813 100644 --- a/spec/support/shared_contexts/models/ci/job_token_scope.rb +++ b/spec/support/shared_contexts/models/ci/job_token_scope.rb @@ -1,21 +1,27 @@ # frozen_string_literal: true -RSpec.shared_context 'with scoped projects' do - let_it_be(:inbound_scoped_project) { create_scoped_project(source_project, direction: :inbound) } - let_it_be(:outbound_scoped_project) { create_scoped_project(source_project, direction: :outbound) } +RSpec.shared_context 'with a project in each allowlist' do + let_it_be(:outbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :outbound) } + + include_context 'with inaccessible projects' +end + +RSpec.shared_context 'with accessible and inaccessible projects' do + let_it_be(:outbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :outbound) } + let_it_be(:inbound_accessible_project) { create_inbound_accessible_project(source_project) } + let_it_be(:fully_accessible_project) { create_inbound_and_outbound_accessible_project(source_project) } + + include_context 'with inaccessible projects' +end + +RSpec.shared_context 'with inaccessible projects' do + let_it_be(:inbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :inbound) } + include_context 'with unscoped projects' +end + +RSpec.shared_context 'with unscoped projects' do let_it_be(:unscoped_project1) { create(:project) } let_it_be(:unscoped_project2) { create(:project) } let_it_be(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project1) } - - def create_scoped_project(source_project, direction:) - create(:project).tap do |scoped_project| - create( - :ci_job_token_project_scope_link, - source_project: source_project, - target_project: scoped_project, - direction: direction - ) - end - end end diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb index 7c37e5189f1168406ed828b4eab4fbe56b0a22ff..f6e10543c840fa1b12269ecc9badf7bedaeedf85 100644 --- a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb +++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb @@ -11,7 +11,7 @@ let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let(:project) { package.project } - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } let(:job_token) { job.token } let(:auth_token) { personal_access_token.token } let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index e0dd08ec50e2910e06adedbd7cbc691d4bb732a2..f63693dbf2603fab065c69f7eb5287956272e411 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'assigns build to package' do context 'with build info' do - let(:job) { create(:ci_build, user: user) } + let(:job) { create(:ci_build, user: user, project: project) } let(:params) { super().merge(build: job) } it 'assigns the pipeline to the package' do