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