Commit 92a89f89 authored by Fabio Pitino's avatar Fabio Pitino Committed by Grzegorz Bizon

Scope JobToken to authorized projects [RUN ALL RSPEC] [RUN AS-IF-FOSS]

parent 85e509c2
...@@ -15,6 +15,10 @@ module Ci ...@@ -15,6 +15,10 @@ module Ci
next unless job next unless job
validate_job!(job) validate_job!(job)
if job.user && Feature.enabled?(:ci_scoped_job_token, job.project, default_enabled: :yaml)
job.user.set_ci_job_token_scope!(job)
end
end end
end end
......
# 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.
module Ci
module JobToken
class ProjectScopeLink < ApplicationRecord
self.table_name = 'ci_job_token_project_scope_links'
belongs_to :source_project, class_name: 'Project'
belongs_to :target_project, class_name: 'Project'
belongs_to :added_by, class_name: 'User'
scope :from_project, ->(project) { where(source_project: project) }
scope :to_project, ->(project) { where(target_project: project) }
validates :source_project, presence: true
validates :target_project, presence: true
validate :not_self_referential_link
private
def not_self_referential_link
return unless source_project && target_project
if source_project == target_project
self.errors.add(:target_project, _("can't be the same as the source project"))
end
end
end
end
end
# frozen_string_literal: true
# This model represents the surface where a CI_JOB_TOKEN can be used.
# A Scope is initialized with the project that the job token belongs to,
# and indicates what are all the other projects that the token could access.
#
# By default a job token can only access its own project, which is the same
# project that defines the scope.
# By adding ScopeLinks to the scope we can allow other projects to be accessed
# by the job token. This works as an allowlist of projects for a job token.
#
# If a project is not included in the scope we should not allow the job user
# to access it since operations using CI_JOB_TOKEN should be considered untrusted.
module Ci
module JobToken
class Scope
attr_reader :source_project
def initialize(project)
@source_project = project
end
def includes?(target_project)
target_project.id == source_project.id ||
Ci::JobToken::ProjectScopeLink.from_project(source_project).to_project(target_project).exists?
end
def all_projects
Project.from_union([
Project.id_in(source_project),
Project.where_exists(
Ci::JobToken::ProjectScopeLink
.from_project(source_project)
.where('projects.id = ci_job_token_project_scope_links.target_project_id'))
], remove_duplicates: false)
end
end
end
end
...@@ -1923,6 +1923,20 @@ class User < ApplicationRecord ...@@ -1923,6 +1923,20 @@ class User < ApplicationRecord
confirmed? && !blocked? && !ghost? confirmed? && !blocked? && !ghost?
end end
# This attribute hosts a Ci::JobToken::Scope object which is set when
# the user is authenticated successfully via CI_JOB_TOKEN.
def ci_job_token_scope
Gitlab::SafeRequestStore[ci_job_token_scope_cache_key]
end
def set_ci_job_token_scope!(job)
Gitlab::SafeRequestStore[ci_job_token_scope_cache_key] = Ci::JobToken::Scope.new(job.project)
end
def from_ci_job_token?
ci_job_token_scope.present?
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
...@@ -2086,6 +2100,10 @@ class User < ApplicationRecord ...@@ -2086,6 +2100,10 @@ class User < ApplicationRecord
def update_highest_role_attribute def update_highest_role_attribute
id id
end end
def ci_job_token_scope_cache_key
"users:#{id}:ci:job_token_scope"
end
end end
User.prepend_mod_with('User') User.prepend_mod_with('User')
...@@ -84,6 +84,10 @@ module PolicyActor ...@@ -84,6 +84,10 @@ module PolicyActor
def password_expired? def password_expired?
false false
end end
def from_ci_job_token?
false
end
end end
PolicyActor.prepend_mod_with('PolicyActor') PolicyActor.prepend_mod_with('PolicyActor')
...@@ -75,6 +75,11 @@ class ProjectPolicy < BasePolicy ...@@ -75,6 +75,11 @@ class ProjectPolicy < BasePolicy
user.is_a?(DeployToken) && user.has_access_to?(project) && user.write_package_registry user.is_a?(DeployToken) && user.has_access_to?(project) && user.write_package_registry
end end
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.includes?(project)
end
with_scope :subject with_scope :subject
condition(:forking_allowed) do condition(:forking_allowed) do
@subject.feature_available?(:forking, @user) @subject.feature_available?(:forking, @user)
...@@ -508,6 +513,8 @@ class ProjectPolicy < BasePolicy ...@@ -508,6 +513,8 @@ class ProjectPolicy < BasePolicy
enable :read_project_for_iids enable :read_project_for_iids
end end
rule { ~project_allowed_for_job_token }.prevent_all
rule { can?(:public_access) }.policy do rule { can?(:public_access) }.policy do
enable :read_package enable :read_package
enable :read_project enable :read_project
......
---
name: ci_scoped_job_token
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62733
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332272
milestone: '14.0'
type: development
group: group::pipeline execution
default_enabled: false
# frozen_string_literal: true
class CreateCiJobTokenProjectScopeLinks < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
def up
with_lock_retries do
create_table :ci_job_token_project_scope_links, if_not_exists: true do |t|
t.belongs_to :source_project, index: false, null: false, foreign_key: { to_table: :projects, on_delete: :cascade }
t.belongs_to :target_project, null: false, foreign_key: { to_table: :projects, on_delete: :cascade }
t.belongs_to :added_by, foreign_key: { to_table: :users, on_delete: :nullify }
t.datetime_with_timezone :created_at, null: false
t.index [:source_project_id, :target_project_id], unique: true, name: 'i_ci_job_token_project_scope_links_on_source_and_target_project'
end
end
end
def down
with_lock_retries do
drop_table :ci_job_token_project_scope_links, if_exists: true
end
end
end
8c0661a42edbdb79be283df0e88879707ef34ba3fe21b6756b21cd99ea9f05de
\ No newline at end of file
...@@ -10777,6 +10777,23 @@ CREATE SEQUENCE ci_job_artifacts_id_seq ...@@ -10777,6 +10777,23 @@ CREATE SEQUENCE ci_job_artifacts_id_seq
ALTER SEQUENCE ci_job_artifacts_id_seq OWNED BY ci_job_artifacts.id; ALTER SEQUENCE ci_job_artifacts_id_seq OWNED BY ci_job_artifacts.id;
CREATE TABLE ci_job_token_project_scope_links (
id bigint NOT NULL,
source_project_id bigint NOT NULL,
target_project_id bigint NOT NULL,
added_by_id bigint,
created_at timestamp with time zone NOT NULL
);
CREATE SEQUENCE ci_job_token_project_scope_links_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE ci_job_token_project_scope_links_id_seq OWNED BY ci_job_token_project_scope_links.id;
CREATE TABLE ci_job_variables ( CREATE TABLE ci_job_variables (
id bigint NOT NULL, id bigint NOT NULL,
key character varying NOT NULL, key character varying NOT NULL,
...@@ -19699,6 +19716,8 @@ ALTER TABLE ONLY ci_instance_variables ALTER COLUMN id SET DEFAULT nextval('ci_i ...@@ -19699,6 +19716,8 @@ ALTER TABLE ONLY ci_instance_variables ALTER COLUMN id SET DEFAULT nextval('ci_i
ALTER TABLE ONLY ci_job_artifacts ALTER COLUMN id SET DEFAULT nextval('ci_job_artifacts_id_seq'::regclass); ALTER TABLE ONLY ci_job_artifacts ALTER COLUMN id SET DEFAULT nextval('ci_job_artifacts_id_seq'::regclass);
ALTER TABLE ONLY ci_job_token_project_scope_links ALTER COLUMN id SET DEFAULT nextval('ci_job_token_project_scope_links_id_seq'::regclass);
ALTER TABLE ONLY ci_job_variables ALTER COLUMN id SET DEFAULT nextval('ci_job_variables_id_seq'::regclass); ALTER TABLE ONLY ci_job_variables ALTER COLUMN id SET DEFAULT nextval('ci_job_variables_id_seq'::regclass);
ALTER TABLE ONLY ci_minutes_additional_packs ALTER COLUMN id SET DEFAULT nextval('ci_minutes_additional_packs_id_seq'::regclass); ALTER TABLE ONLY ci_minutes_additional_packs ALTER COLUMN id SET DEFAULT nextval('ci_minutes_additional_packs_id_seq'::regclass);
...@@ -20881,6 +20900,9 @@ ALTER TABLE ONLY ci_instance_variables ...@@ -20881,6 +20900,9 @@ ALTER TABLE ONLY ci_instance_variables
ALTER TABLE ONLY ci_job_artifacts ALTER TABLE ONLY ci_job_artifacts
ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY (id); ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ci_job_token_project_scope_links
ADD CONSTRAINT ci_job_token_project_scope_links_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ci_job_variables ALTER TABLE ONLY ci_job_variables
ADD CONSTRAINT ci_job_variables_pkey PRIMARY KEY (id); ADD CONSTRAINT ci_job_variables_pkey PRIMARY KEY (id);
...@@ -22335,6 +22357,8 @@ CREATE INDEX finding_evidences_on_vulnerability_occurrence_id ON vulnerability_f ...@@ -22335,6 +22357,8 @@ CREATE INDEX finding_evidences_on_vulnerability_occurrence_id ON vulnerability_f
CREATE INDEX finding_links_on_vulnerability_occurrence_id ON vulnerability_finding_links USING btree (vulnerability_occurrence_id); CREATE INDEX finding_links_on_vulnerability_occurrence_id ON vulnerability_finding_links USING btree (vulnerability_occurrence_id);
CREATE UNIQUE INDEX i_ci_job_token_project_scope_links_on_source_and_target_project ON ci_job_token_project_scope_links USING btree (source_project_id, target_project_id);
CREATE INDEX idx_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id); CREATE INDEX idx_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id);
CREATE INDEX idx_audit_events_part_on_entity_id_desc_author_id_created_at ON ONLY audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at); CREATE INDEX idx_audit_events_part_on_entity_id_desc_author_id_created_at ON ONLY audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at);
...@@ -22815,6 +22839,10 @@ CREATE INDEX index_ci_job_artifacts_on_project_id ON ci_job_artifacts USING btre ...@@ -22815,6 +22839,10 @@ CREATE INDEX index_ci_job_artifacts_on_project_id ON ci_job_artifacts USING btre
CREATE INDEX index_ci_job_artifacts_on_project_id_for_security_reports ON ci_job_artifacts USING btree (project_id) WHERE (file_type = ANY (ARRAY[5, 6, 7, 8])); CREATE INDEX index_ci_job_artifacts_on_project_id_for_security_reports ON ci_job_artifacts USING btree (project_id) WHERE (file_type = ANY (ARRAY[5, 6, 7, 8]));
CREATE INDEX index_ci_job_token_project_scope_links_on_added_by_id ON ci_job_token_project_scope_links USING btree (added_by_id);
CREATE INDEX index_ci_job_token_project_scope_links_on_target_project_id ON ci_job_token_project_scope_links USING btree (target_project_id);
CREATE INDEX index_ci_job_variables_on_job_id ON ci_job_variables USING btree (job_id); CREATE INDEX index_ci_job_variables_on_job_id ON ci_job_variables USING btree (job_id);
CREATE UNIQUE INDEX index_ci_job_variables_on_key_and_job_id ON ci_job_variables USING btree (key, job_id); CREATE UNIQUE INDEX index_ci_job_variables_on_key_and_job_id ON ci_job_variables USING btree (key, job_id);
...@@ -26493,6 +26521,9 @@ ALTER TABLE ONLY metrics_dashboard_annotations ...@@ -26493,6 +26521,9 @@ ALTER TABLE ONLY metrics_dashboard_annotations
ALTER TABLE ONLY wiki_page_slugs ALTER TABLE ONLY wiki_page_slugs
ADD CONSTRAINT fk_rails_358b46be14 FOREIGN KEY (wiki_page_meta_id) REFERENCES wiki_page_meta(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_358b46be14 FOREIGN KEY (wiki_page_meta_id) REFERENCES wiki_page_meta(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_job_token_project_scope_links
ADD CONSTRAINT fk_rails_35f7f506ce FOREIGN KEY (added_by_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY board_labels ALTER TABLE ONLY board_labels
ADD CONSTRAINT fk_rails_362b0600a3 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_362b0600a3 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
...@@ -26628,6 +26659,9 @@ ALTER TABLE ONLY upcoming_reconciliations ...@@ -26628,6 +26659,9 @@ ALTER TABLE ONLY upcoming_reconciliations
ALTER TABLE ONLY ci_pipeline_artifacts ALTER TABLE ONLY ci_pipeline_artifacts
ADD CONSTRAINT fk_rails_4a70390ca6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_4a70390ca6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_job_token_project_scope_links
ADD CONSTRAINT fk_rails_4b2ee3290b FOREIGN KEY (source_project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY group_deletion_schedules ALTER TABLE ONLY group_deletion_schedules
ADD CONSTRAINT fk_rails_4b8c694a6c FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_4b8c694a6c FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
...@@ -26844,6 +26878,9 @@ ALTER TABLE ONLY vulnerability_finding_evidence_headers ...@@ -26844,6 +26878,9 @@ ALTER TABLE ONLY vulnerability_finding_evidence_headers
ALTER TABLE ONLY geo_hashed_storage_migrated_events ALTER TABLE ONLY geo_hashed_storage_migrated_events
ADD CONSTRAINT fk_rails_687ed7d7c5 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_687ed7d7c5 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_job_token_project_scope_links
ADD CONSTRAINT fk_rails_6904b38465 FOREIGN KEY (target_project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY plan_limits ALTER TABLE ONLY plan_limits
ADD CONSTRAINT fk_rails_69f8b6184f FOREIGN KEY (plan_id) REFERENCES plans(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_69f8b6184f FOREIGN KEY (plan_id) REFERENCES plans(id) ON DELETE CASCADE;
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe API::Internal::AppSec::Dast::SiteValidations do RSpec.describe API::Internal::AppSec::Dast::SiteValidations do
include AfterNextHelpers
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project]) } let_it_be(:developer) { create(:user, developer_projects: [project]) }
let_it_be(:site_validation) { create(:dast_site_validation, dast_site_token: create(:dast_site_token, project: project)) } let_it_be(:site_validation) { create(:dast_site_validation, dast_site_token: create(:dast_site_token, project: project)) }
...@@ -66,11 +68,23 @@ RSpec.describe API::Internal::AppSec::Dast::SiteValidations do ...@@ -66,11 +68,23 @@ RSpec.describe API::Internal::AppSec::Dast::SiteValidations do
context 'when site validation and job are associated with different projects' do context 'when site validation and job are associated with different projects' do
let_it_be(:job) { create(:ci_build, :running, user: developer) } let_it_be(:job) { create(:ci_build, :running, user: developer) }
it 'returns 400 and a contextual error message', :aggregate_failures do it 'returns 403', :aggregate_failures do
subject subject
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response).to eq('message' => '400 Bad request - Project mismatch') end
context 'when the job project belongs to the same job token scope' do
before do
allow_next(Ci::JobToken::Scope).to receive(:includes?).with(project).and_return(true)
end
it 'returns 400 and a contextual error message', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('message' => '400 Bad request - Project mismatch')
end
end end
end end
......
...@@ -3,18 +3,25 @@ ...@@ -3,18 +3,25 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe API::Triggers do RSpec.describe API::Triggers do
include AfterNextHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :auto_devops, creator: user) } let_it_be(:project) { create(:project, :repository, :auto_devops, creator: user) }
let_it_be(:other_project) { create(:project) }
describe 'POST /projects/:project_id/trigger/pipeline' do describe 'POST /projects/:project_id/trigger/pipeline' do
context 'when triggering a pipeline from a job token' do context 'when triggering a pipeline from a job token' do
let(:other_job) { create(:ci_build, :running, user: other_user) } let(:other_job) { create(:ci_build, :running, user: other_user, project: other_project) }
let(:params) { { ref: 'refs/heads/other-branch' } } let(:params) { { ref: 'refs/heads/other-branch' } }
subject do subject do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{other_job.token}"), params: params post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{other_job.token}"), params: params
end end
before do
allow_next(Ci::JobToken::Scope).to receive(:includes?).with(project).and_return(true)
end
context 'without user' do context 'without user' do
let(:other_user) { nil } let(:other_user) { nil }
...@@ -70,6 +77,21 @@ RSpec.describe API::Triggers do ...@@ -70,6 +77,21 @@ RSpec.describe API::Triggers do
) )
end end
context 'when project is not in the job token scope' do
before do
allow_next(Ci::JobToken::Scope)
.to receive(:includes?)
.with(project)
.and_return(false)
end
it 'forbids to create a pipeline' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when build is complete' do context 'when build is complete' do
before do before do
other_job.success other_job.success
......
...@@ -38009,6 +38009,9 @@ msgstr "" ...@@ -38009,6 +38009,9 @@ msgstr ""
msgid "can't be enabled because signed commits are required for this project" msgid "can't be enabled because signed commits are required for this project"
msgstr "" msgstr ""
msgid "can't be the same as the source project"
msgstr ""
msgid "can't include: %{invalid_storages}" msgid "can't include: %{invalid_storages}"
msgstr "" msgstr ""
......
# frozen_string_literal: true
FactoryBot.define do
factory :ci_job_token_project_scope_link, class: 'Ci::JobToken::ProjectScopeLink' do
association :source_project, factory: :project
association :target_project, factory: :project
association :added_by, factory: :user
end
end
...@@ -66,6 +66,7 @@ RSpec.describe 'factories' do ...@@ -66,6 +66,7 @@ RSpec.describe 'factories' do
# associations must be unique and cannot be reused, or the factory default # associations must be unique and cannot be reused, or the factory default
# is being mutated. # is being mutated.
skip_factory_defaults = %i[ skip_factory_defaults = %i[
ci_job_token_project_scope_link
evidence evidence
exported_protected_branch exported_protected_branch
fork_network_member fork_network_member
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::AuthJobFinder do RSpec.describe Ci::AuthJobFinder do
let_it_be(:job, reload: true) { create(:ci_build, status: :running) } let_it_be(:user, reload: true) { create(:user) }
let_it_be(:job, reload: true) { create(:ci_build, status: :running, user: user) }
let(:token) { job.token } let(:token) { job.token }
...@@ -55,10 +56,31 @@ RSpec.describe Ci::AuthJobFinder do ...@@ -55,10 +56,31 @@ RSpec.describe Ci::AuthJobFinder do
describe '#execute' do describe '#execute' do
subject(:execute) { finder.execute } subject(:execute) { finder.execute }
before do context 'when job is not running' do
job.success! before do
job.success!
end
it { is_expected.to be_nil }
end end
it { is_expected.to be_nil } context 'when job is running', :request_store do
it 'sets ci_job_token_scope on the job user', :aggregate_failures do
expect(subject).to eq(job)
expect(subject.user).to be_from_ci_job_token
expect(subject.user.ci_job_token_scope.source_project).to eq(job.project)
end
context 'when feature flag ci_scoped_job_token is disabled' do
before do
stub_feature_flags(ci_scoped_job_token: false)
end
it 'does not set ci_job_token_scope on the job user' do
expect(subject).to eq(job)
expect(subject.user).not_to be_from_ci_job_token
end
end
end
end end
end end
...@@ -50,7 +50,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do ...@@ -50,7 +50,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end end
shared_examples 'find user from job token' do |without_job_token_allowed| shared_examples 'find user from job token' do |without_job_token_allowed|
context 'when route is allowed to be authenticated' do context 'when route is allowed to be authenticated', :request_store do
let(:route_authentication_setting) { { job_token_allowed: true } } let(:route_authentication_setting) { { job_token_allowed: true } }
context 'for an invalid token' do context 'for an invalid token' do
...@@ -68,6 +68,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do ...@@ -68,6 +68,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do
it 'return user' do it 'return user' do
expect(subject).to eq(user) expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job expect(@current_authenticated_job).to eq job
expect(subject).to be_from_ci_job_token
expect(subject.ci_job_token_scope.source_project).to eq(job.project)
end end
end end
...@@ -81,7 +83,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do ...@@ -81,7 +83,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end end
end end
context 'when route is not allowed to be authenticated' do context 'when route is not allowed to be authenticated', :request_store do
let(:route_authentication_setting) { { job_token_allowed: false } } let(:route_authentication_setting) { { job_token_allowed: false } }
context 'with a running job' do context 'with a running job' do
...@@ -96,6 +98,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do ...@@ -96,6 +98,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do
it 'returns the user' do it 'returns the user' do
expect(subject).to eq(user) expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job expect(@current_authenticated_job).to eq job
expect(subject).to be_from_ci_job_token
expect(subject.ci_job_token_scope.source_project).to eq(job.project)
end end
else else
it 'returns nil' do it 'returns nil' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::JobToken::ProjectScopeLink do
it { is_expected.to belong_to(:source_project) }
it { is_expected.to belong_to(:target_project) }
it { is_expected.to belong_to(:added_by) }
let_it_be(:project) { create(:project) }
describe 'unique index' do
let!(:link) { create(:ci_job_token_project_scope_link) }
it 'raises an error' do
expect do
create(:ci_job_token_project_scope_link,
source_project: link.source_project,
target_project: link.target_project)
end.to raise_error(ActiveRecord::RecordNotUnique)
end
end
describe 'validations' do
it 'must have a source project', :aggregate_failures do
link = build(:ci_job_token_project_scope_link, source_project: nil)
expect(link).not_to be_valid
expect(link.errors[:source_project]).to contain_exactly("can't be blank")
end
it 'must have a target project', :aggregate_failures do
link = build(:ci_job_token_project_scope_link, target_project: nil)
expect(link).not_to be_valid
expect(link.errors[:target_project]).to contain_exactly("can't be blank")
end
it 'must have a target project different than source project', :aggregate_failures do
link = build(:ci_job_token_project_scope_link, target_project: project, source_project: project)
expect(link).not_to be_valid
expect(link.errors[:target_project]).to contain_exactly("can't be the same as the source project")
end
end
describe '.from_project' do
subject { described_class.from_project(project) }
let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) }
let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) }
it 'returns only the links having the given source project' do
expect(subject).to contain_exactly(source_link)
end
end
describe '.to_project' do
subject { described_class.to_project(project) }
let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) }
let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) }
it 'returns only the links having the given target project' do
expect(subject).to contain_exactly(target_link)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::JobToken::Scope do
let_it_be(:project) { create(:project) }
let(:scope) { described_class.new(project) }
describe '#all_projects' do
subject(:all_projects) { scope.all_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(project)
end
end
context 'when other projects are added to the scope' do
let_it_be(:scoped_project) { create(:project) }
let_it_be(:unscoped_project) { create(:project) }
let!(:link_in_scope) { create(:ci_job_token_project_scope_link, source_project: project, target_project: scoped_project) }
let!(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project) }
it 'returns all projects that can be accessed from a given scope' do
expect(subject).to contain_exactly(project, scoped_project)
end
end
end
describe 'includes?' do
subject { scope.includes?(target_project) }
context 'when param is the project defining the scope' do
let(:target_project) { project }
it { is_expected.to be_truthy }
end
context 'when param is a project in scope' do
let(:target_link) { create(:ci_job_token_project_scope_link, source_project: project) }
let(:target_project) { target_link.target_project }
it { is_expected.to be_truthy }
end
context 'when param is a project in another scope' do
let(:scope_link) { create(:ci_job_token_project_scope_link) }
let(:target_project) { scope_link.target_project }
it { is_expected.to be_falsey }
end
end
end
...@@ -1385,4 +1385,53 @@ RSpec.describe ProjectPolicy do ...@@ -1385,4 +1385,53 @@ RSpec.describe ProjectPolicy do
end end
end end
end end
describe 'when user is authenticated via CI_JOB_TOKEN', :request_store do
let(:current_user) { developer }
let(:job) { build_stubbed(:ci_build, project: scope_project, user: current_user) }
before do
current_user.set_ci_job_token_scope!(job)
end
context 'when accessing a private project' do
let(:project) { private_project }
context 'when the job token comes from the same project' do
let(:scope_project) { project }
it { is_expected.to be_allowed(:developer_access) }
end
context 'when the job token comes from another project' do
let(:scope_project) { create(:project, :private) }
before do
scope_project.add_developer(current_user)
end
it { is_expected.to be_disallowed(:guest_access) }
end
end
context 'when accessing a public project' do
let(:project) { public_project }
context 'when the job token comes from the same project' do
let(:scope_project) { project }
it { is_expected.to be_allowed(:developer_access) }
end
context 'when the job token comes from another project' do
let(:scope_project) { create(:project, :private) }
before do
scope_project.add_developer(current_user)
end
it { is_expected.to be_disallowed(:public_access) }
end
end
end
end end
...@@ -18,7 +18,7 @@ RSpec.describe API::GenericPackages do ...@@ -18,7 +18,7 @@ RSpec.describe API::GenericPackages do
let_it_be(:project_deploy_token_wo) { create(:project_deploy_token, deploy_token: deploy_token_wo, project: project) } let_it_be(:project_deploy_token_wo) { create(:project_deploy_token, deploy_token: deploy_token_wo, project: project) }
let(:user) { personal_access_token.user } let(:user) { personal_access_token.user }
let(:ci_build) { create(:ci_build, :running, user: user) } let(:ci_build) { create(:ci_build, :running, user: user, project: project) }
def auth_header def auth_header
return {} if user_role == :anonymous return {} if user_role == :anonymous
......
...@@ -11,7 +11,7 @@ RSpec.describe API::GoProxy do ...@@ -11,7 +11,7 @@ RSpec.describe API::GoProxy do
let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" } let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" }
let_it_be(:oauth) { create :oauth_access_token, scopes: 'api', resource_owner: user } let_it_be(:oauth) { create :oauth_access_token, scopes: 'api', resource_owner: user }
let_it_be(:job) { create :ci_build, user: user, status: :running } let_it_be(:job) { create :ci_build, user: user, status: :running, project: project }
let_it_be(:pa_token) { create :personal_access_token, user: user } let_it_be(:pa_token) { create :personal_access_token, user: user }
let_it_be(:modules) do let_it_be(:modules) do
......
...@@ -15,7 +15,7 @@ RSpec.describe API::MavenPackages do ...@@ -15,7 +15,7 @@ RSpec.describe API::MavenPackages do
let_it_be(:package_file) { package.package_files.with_file_name_like('%.xml').first } let_it_be(:package_file) { package.package_files.with_file_name_like('%.xml').first }
let_it_be(:jar_file) { package.package_files.with_file_name_like('%.jar').first } let_it_be(:jar_file) { package.package_files.with_file_name_like('%.jar').first }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) } let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running, project: project) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) } let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
......
...@@ -78,7 +78,7 @@ RSpec.describe API::NpmProjectPackages do ...@@ -78,7 +78,7 @@ RSpec.describe API::NpmProjectPackages do
context 'with a job token for a different user' do context 'with a job token for a different user' do
let_it_be(:other_user) { create(:user) } let_it_be(:other_user) { create(:user) }
let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) } let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user, project: project) }
let(:headers) { build_token_auth_header(other_job.token) } let(:headers) { build_token_auth_header(other_job.token) }
......
...@@ -192,7 +192,7 @@ RSpec.describe API::NugetProjectPackages do ...@@ -192,7 +192,7 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'deploy token for package uploads' it_behaves_like 'deploy token for package uploads'
it_behaves_like 'job token for package uploads', authorize_endpoint: true do it_behaves_like 'job token for package uploads', authorize_endpoint: true do
let_it_be(:job) { create(:ci_build, :running, user: user) } let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
end end
it_behaves_like 'rejects nuget access with unknown target id' it_behaves_like 'rejects nuget access with unknown target id'
...@@ -260,7 +260,7 @@ RSpec.describe API::NugetProjectPackages do ...@@ -260,7 +260,7 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'deploy token for package uploads' it_behaves_like 'deploy token for package uploads'
it_behaves_like 'job token for package uploads' do it_behaves_like 'job token for package uploads' do
let_it_be(:job) { create(:ci_build, :running, user: user) } let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
end end
it_behaves_like 'rejects nuget access with unknown target id' it_behaves_like 'rejects nuget access with unknown target id'
......
...@@ -13,7 +13,7 @@ RSpec.describe API::PypiPackages do ...@@ -13,7 +13,7 @@ RSpec.describe API::PypiPackages do
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:job) { create(:ci_build, :running, user: user) } let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
let(:headers) { {} } let(:headers) { {} }
context 'simple API endpoint' do context 'simple API endpoint' do
......
...@@ -775,7 +775,7 @@ RSpec.describe API::Releases do ...@@ -775,7 +775,7 @@ RSpec.describe API::Releases do
end end
context 'when using JOB-TOKEN auth' do context 'when using JOB-TOKEN auth' do
let(:job) { create(:ci_build, user: maintainer) } let(:job) { create(:ci_build, user: maintainer, project: project) }
let(:params) do let(:params) do
{ {
name: 'Another release', name: 'Another release',
......
...@@ -10,7 +10,7 @@ RSpec.describe API::RubygemPackages do ...@@ -10,7 +10,7 @@ RSpec.describe API::RubygemPackages do
let_it_be_with_reload(:project) { create(:project) } let_it_be_with_reload(:project) { create(:project) }
let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user } let_it_be(:user) { personal_access_token.user }
let_it_be(:job) { create(:ci_build, :running, user: user) } let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:headers) { {} } let_it_be(:headers) { {} }
......
...@@ -12,7 +12,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do ...@@ -12,7 +12,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
let_it_be(:package) { create(:terraform_module_package, project: project) } let_it_be(:package) { create(:terraform_module_package, project: project) }
let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user } let_it_be(:user) { personal_access_token.user }
let_it_be(:job) { create(:ci_build, :running, user: user) } let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
......
...@@ -883,10 +883,10 @@ RSpec.describe 'Git HTTP requests' do ...@@ -883,10 +883,10 @@ RSpec.describe 'Git HTTP requests' do
context 'when admin mode is enabled', :enable_admin_mode do context 'when admin mode is enabled', :enable_admin_mode do
it_behaves_like 'can download code only' it_behaves_like 'can download code only'
it 'downloads from other project get status 403' do it 'downloads from other project get status 404' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
......
...@@ -569,7 +569,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -569,7 +569,7 @@ RSpec.describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
# I'm not sure what this tests that is different from the previous test # I'm not sure what this tests that is different from the previous test
it_behaves_like 'LFS http 403 response' it_behaves_like 'LFS http 404 response'
end end
end end
...@@ -1043,7 +1043,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -1043,7 +1043,7 @@ RSpec.describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
# I'm not sure what this tests that is different from the previous test # I'm not sure what this tests that is different from the previous test
it_behaves_like 'LFS http 403 response' it_behaves_like 'LFS http 404 response'
end end
end end
......
...@@ -8,11 +8,11 @@ RSpec.shared_context 'conan api setup' do ...@@ -8,11 +8,11 @@ RSpec.shared_context 'conan api setup' do
let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user } let_it_be(:user) { personal_access_token.user }
let_it_be(:base_secret) { SecureRandom.base64(64) } let_it_be(:base_secret) { SecureRandom.base64(64) }
let_it_be(:job) { create(:ci_build, :running, user: user) }
let_it_be(:job_token) { job.token }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let(:project) { package.project } let(:project) { package.project }
let(:job) { create(:ci_build, :running, user: user, project: project) }
let(:job_token) { job.token }
let(:auth_token) { personal_access_token.token } let(:auth_token) { personal_access_token.token }
let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
......
...@@ -11,7 +11,7 @@ RSpec.shared_context 'npm api setup' do ...@@ -11,7 +11,7 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") } let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) } let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) } let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running, project: project) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment