diff --git a/app/graphql/types/ci/job_token_scope_type.rb b/app/graphql/types/ci/job_token_scope_type.rb index 37c0af944a7b25dd7d0f2c38d51a245158a0a56d..9c9c7ccb8d12f024f85f00cd1f5c4468bac50c18 100644 --- a/app/graphql/types/ci/job_token_scope_type.rb +++ b/app/graphql/types/ci/job_token_scope_type.rb @@ -11,7 +11,7 @@ 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 end end # rubocop: enable Graphql/AuthorizeTypes 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/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..07c2755a3f724a2dd18b2419b2f20c2dcf8a4984 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 @@ -45,7 +45,7 @@ it 'adds target project to the job token scope' 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 the service returns an error' do 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/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb index 45083d64393a7741d2ec8fc6952b12f602952c88..c69dcba765a1f59bb33a0c77f7acef6d71b66a35 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 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/support/helpers/ci/job_token_scope_helpers.rb b/spec/support/helpers/ci/job_token_scope_helpers.rb new file mode 100644 index 0000000000000000000000000000000000000000..09084bc871542ce51c7655b087d6d9856ac265cd --- /dev/null +++ b/spec/support/helpers/ci/job_token_scope_helpers.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Ci + module JobTokenScopeHelpers + def create_project_in_allowlist(root_project, direction:) + create(:project).tap do |scoped_project| + create( + :ci_job_token_project_scope_link, + source_project: root_project, + target_project: scoped_project, + direction: direction + ) + 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