diff options
-rw-r--r-- | app/models/ci/pipeline.rb | 14 | ||||
-rw-r--r-- | app/models/ci/pipeline_schedule.rb | 2 | ||||
-rw-r--r-- | app/policies/ci/build_policy.rb | 8 | ||||
-rw-r--r-- | app/policies/ci/pipeline_policy.rb | 10 | ||||
-rw-r--r-- | app/services/ci/create_pipeline_service.rb | 30 | ||||
-rw-r--r-- | app/services/ci/create_trigger_request_service.rb | 3 | ||||
-rw-r--r-- | changelogs/unreleased/30634-protected-pipeline.yml | 5 | ||||
-rw-r--r-- | lib/api/triggers.rb | 9 | ||||
-rw-r--r-- | lib/api/v3/triggers.rb | 7 | ||||
-rw-r--r-- | lib/ci/api/triggers.rb | 7 | ||||
-rw-r--r-- | lib/gitlab/user_access.rb | 4 | ||||
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 97 | ||||
-rw-r--r-- | spec/policies/ci/build_policy_spec.rb | 52 | ||||
-rw-r--r-- | spec/policies/ci/pipeline_policy_spec.rb | 47 | ||||
-rw-r--r-- | spec/requests/api/triggers_spec.rb | 3 | ||||
-rw-r--r-- | spec/requests/api/v3/triggers_spec.rb | 3 | ||||
-rw-r--r-- | spec/requests/ci/api/triggers_spec.rb | 14 | ||||
-rw-r--r-- | spec/services/ci/create_pipeline_service_spec.rb | 115 | ||||
-rw-r--r-- | spec/services/ci/create_trigger_request_service_spec.rb | 12 | ||||
-rw-r--r-- | spec/workers/post_receive_spec.rb | 1 |
20 files changed, 374 insertions, 69 deletions
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 364858964b0..bea2ec1e18c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -164,6 +164,20 @@ module Ci where.not(duration: nil).sum(:duration) end + def self.allowed_to_create?(user, project, ref) + repo = project.repository + access = Gitlab::UserAccess.new(user, project: project) + + Ability.allowed?(user, :create_pipeline, project) && + if repo.ref_exists?("#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref}") + access.can_push_or_merge_to_branch?(ref) + elsif repo.ref_exists?("#{Gitlab::Git::TAG_REF_PREFIX}#{ref}") + access.can_create_tag?(ref) + else + false + end + end + def self.internal_sources sources.reject { |source| source == "external" }.values end diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 45d8cd34359..eaca2774bf9 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -37,7 +37,7 @@ module Ci end def runnable_by_owner? - Ability.allowed?(owner, :create_pipeline, project) + Ci::Pipeline.allowed_to_create?(owner, project, ref) end def set_next_run_at diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index a886efc1360..129ed756477 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,13 +1,11 @@ module Ci class BuildPolicy < CommitStatusPolicy - condition(:protected_action) do - next false unless @subject.action? - + condition(:user_cannot_update) do !::Gitlab::UserAccess .new(@user, project: @subject.project) - .can_merge_to_branch?(@subject.ref) + .can_push_or_merge_to_branch?(@subject.ref) end - rule { protected_action }.prevent :update_build + rule { user_cannot_update }.prevent :update_build end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index a2dde95dbc8..73b5a40c7fc 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -1,5 +1,13 @@ module Ci class PipelinePolicy < BasePolicy - delegate { @subject.project } + delegate { pipeline.project } + + condition(:user_cannot_update) do + !::Gitlab::UserAccess + .new(@user, project: @subject.project) + .can_push_or_merge_to_branch?(@subject.ref) + end + + rule { user_cannot_update }.prevent :update_pipeline end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 942145c4a8c..db12116b3ae 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,6 +27,10 @@ module Ci return error('Reference not found') end + unless triggering_user_allowed_for_ref?(trigger_request, ref) + return error("Insufficient permissions for protected #{ref}") + end + unless commit return error('Commit not found') end @@ -47,6 +51,20 @@ module Ci return error('No stages / jobs for this pipeline.') end + process! + end + + private + + def triggering_user_allowed_for_ref?(trigger_request, ref) + triggering_user = current_user || trigger_request.trigger.owner + + (triggering_user && + Ci::Pipeline.allowed_to_create?(triggering_user, project, ref)) || + !project.protected_for?(ref) + end + + def process! Ci::Pipeline.transaction do update_merge_requests_head_pipeline if pipeline.save @@ -62,8 +80,6 @@ module Ci pipeline.tap(&:process!) end - private - def update_merge_requests_head_pipeline return unless pipeline.latest? @@ -113,11 +129,17 @@ module Ci end def branch? - project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref) + return @is_branch if defined?(@is_branch) + + @is_branch = + project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref) end def tag? - project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref) + return @is_tag if defined?(@is_tag) + + @is_tag = + project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref) end def ref diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index cf3d4aee2bc..90f75606ddf 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -6,7 +6,8 @@ module Ci pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref) .execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) - trigger_request if pipeline.persisted? + trigger_request.pipeline = pipeline + trigger_request end end end diff --git a/changelogs/unreleased/30634-protected-pipeline.yml b/changelogs/unreleased/30634-protected-pipeline.yml new file mode 100644 index 00000000000..e46538e5b46 --- /dev/null +++ b/changelogs/unreleased/30634-protected-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Disallow running the pipeline if ref is protected and user cannot merge the + branch or create the tag +merge_request: 11910 +author: diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index a9f2ca2608e..9e444563fdf 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -28,11 +28,12 @@ module API # create request and trigger builds trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) - if trigger_request - present trigger_request.pipeline, with: Entities::Pipeline + pipeline = trigger_request.pipeline + + if pipeline.persisted? + present pipeline, with: Entities::Pipeline else - errors = 'No pipeline created' - render_api_error!(errors, 400) + render_validation_error!(pipeline) end end diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb index a23d6b6b48c..7e75c579528 100644 --- a/lib/api/v3/triggers.rb +++ b/lib/api/v3/triggers.rb @@ -29,11 +29,12 @@ module API # create request and trigger builds trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) - if trigger_request + pipeline = trigger_request.pipeline + + if pipeline.persisted? present trigger_request, with: ::API::V3::Entities::TriggerRequest else - errors = 'No builds created' - render_api_error!(errors, 400) + render_validation_error!(pipeline) end end diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb index 6e622601680..0e5174e13ab 100644 --- a/lib/ci/api/triggers.rb +++ b/lib/ci/api/triggers.rb @@ -25,11 +25,12 @@ module Ci # create request and trigger builds trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref], variables) - if trigger_request + pipeline = trigger_request.pipeline + + if pipeline.persisted? present trigger_request, with: Entities::TriggerRequest else - errors = 'No builds created' - render_api_error!(errors, 400) + render_validation_error!(pipeline) end end end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 3b922da7ced..bb05c474fa2 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -48,6 +48,10 @@ module Gitlab end end + def can_push_or_merge_to_branch?(ref) + can_push_to_branch?(ref) || can_merge_to_branch?(ref) + end + def can_push_to_branch?(ref) return false unless can_access_git? diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 55d85a6e228..776a674a6d9 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -28,6 +28,103 @@ describe Ci::Pipeline, models: true do it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } + describe '.allowed_to_create?' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:ref) { 'master' } + + subject { described_class.allowed_to_create?(user, project, ref) } + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it { is_expected.to be_truthy } + + context 'when the branch is protected' do + let!(:protected_branch) do + create(:protected_branch, project: project, name: ref) + end + + it { is_expected.to be_falsey } + + context 'when developers are allowed to merge' do + let!(:protected_branch) do + create(:protected_branch, + :developers_can_merge, + project: project, + name: ref) + end + + it { is_expected.to be_truthy } + end + end + + context 'when the tag is protected' do + let(:ref) { 'v1.0.0' } + + let!(:protected_tag) do + create(:protected_tag, project: project, name: ref) + end + + it { is_expected.to be_falsey } + + context 'when developers are allowed to create the tag' do + let!(:protected_tag) do + create(:protected_tag, + :developers_can_create, + project: project, + name: ref) + end + + it { is_expected.to be_truthy } + end + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it { is_expected.to be_truthy } + + context 'when the branch is protected' do + let!(:protected_branch) do + create(:protected_branch, project: project, name: ref) + end + + it { is_expected.to be_truthy } + end + + context 'when the tag is protected' do + let(:ref) { 'v1.0.0' } + + let!(:protected_tag) do + create(:protected_tag, project: project, name: ref) + end + + it { is_expected.to be_truthy } + + context 'when no one can create the tag' do + let!(:protected_tag) do + create(:protected_tag, + :no_one_can_create, + project: project, + name: ref) + end + + it { is_expected.to be_falsey } + end + end + end + + context 'when owner cannot create pipeline' do + it { is_expected.to be_falsey } + end + end + describe '#source' do context 'when creating new pipeline' do let(:pipeline) do diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index ace95ac7067..2a8e6653eb8 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -96,55 +96,37 @@ describe Ci::BuildPolicy, :models do end end - describe 'rules for manual actions' do + describe 'rules for protected branch' do let(:project) { create(:project) } before do project.add_developer(user) - end - - context 'when branch build is assigned to is protected' do - before do - create(:protected_branch, :no_one_can_push, - name: 'some-ref', project: project) - end - context 'when build is a manual action' do - let(:build) do - create(:ci_build, :manual, ref: 'some-ref', pipeline: pipeline) - end - - it 'does not include ability to update build' do - expect(policy).to be_disallowed :update_build - end - end + create(:protected_branch, branch_policy, + name: build.ref, project: project) + end - context 'when build is not a manual action' do - let(:build) do - create(:ci_build, ref: 'some-ref', pipeline: pipeline) - end + context 'when no one can push or merge to the branch' do + let(:branch_policy) { :no_one_can_push } - it 'includes ability to update build' do - expect(policy).to be_allowed :update_build - end + it 'does not include ability to update build' do + expect(policies).to be_disallowed :update_build end end - context 'when branch build is assigned to is not protected' do - context 'when build is a manual action' do - let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + context 'when developers can push to the branch' do + let(:branch_policy) { :developers_can_push } - it 'includes ability to update build' do - expect(policy).to be_allowed :update_build - end + it 'includes ability to update build' do + expect(policies).to be_allowed :update_build end + end - context 'when build is not a manual action' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when developers can push to the branch' do + let(:branch_policy) { :developers_can_merge } - it 'includes ability to update build' do - expect(policy).to be_allowed :update_build - end + it 'includes ability to update build' do + expect(policies).to be_allowed :update_build end end end diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb new file mode 100644 index 00000000000..db09be96875 --- /dev/null +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Ci::PipelinePolicy, :models do + let(:user) { create(:user) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + let(:policies) do + described_class.abilities(user, pipeline).to_set + end + + describe 'rules' do + describe 'rules for protected branch' do + let(:project) { create(:project) } + + before do + project.add_developer(user) + + create(:protected_branch, branch_policy, + name: pipeline.ref, project: project) + end + + context 'when no one can push or merge to the branch' do + let(:branch_policy) { :no_one_can_push } + + it 'does not include ability to update pipeline' do + expect(policies).to be_disallowed :update_pipeline + end + end + + context 'when developers can push to the branch' do + let(:branch_policy) { :developers_can_push } + + it 'includes ability to update pipeline' do + expect(policies).to be_allowed :update_pipeline + end + end + + context 'when developers can push to the branch' do + let(:branch_policy) { :developers_can_merge } + + it 'includes ability to update pipeline' do + expect(policies).to be_allowed :update_pipeline + end + end + end + end +end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 16ddade27d9..c2636b6614e 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -61,7 +61,8 @@ describe API::Triggers do post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch') expect(response).to have_http_status(400) - expect(json_response['message']).to eq('No pipeline created') + expect(json_response['message']['base']) + .to contain_exactly('Reference not found') end context 'Validates variables' do diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb index d3de6bf13bc..60212660fb6 100644 --- a/spec/requests/api/v3/triggers_spec.rb +++ b/spec/requests/api/v3/triggers_spec.rb @@ -52,7 +52,8 @@ describe API::V3::Triggers do it 'returns bad request with no builds created if there\'s no commit for that ref' do post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') expect(response).to have_http_status(400) - expect(json_response['message']).to eq('No builds created') + expect(json_response['message']['base']) + .to contain_exactly('Reference not found') end context 'Validates variables' do diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index 26b03c0f148..e481ca916ab 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -5,7 +5,14 @@ describe Ci::API::Triggers do let!(:trigger_token) { 'secure token' } let!(:project) { create(:project, :repository, ci_id: 10) } let!(:project2) { create(:empty_project, ci_id: 11) } - let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } + + let!(:trigger) do + create(:ci_trigger, + project: project, + token: trigger_token, + owner: create(:user)) + end + let(:options) do { token: trigger_token @@ -14,6 +21,8 @@ describe Ci::API::Triggers do before do stub_ci_pipeline_to_return_yaml_file + + project.add_developer(trigger.owner) end context 'Handles errors' do @@ -47,7 +56,8 @@ describe Ci::API::Triggers do it 'returns bad request with no builds created if there\'s no commit for that ref' do post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options expect(response).to have_http_status(400) - expect(json_response['message']).to eq('No builds created') + expect(json_response['message']['base']) + .to contain_exactly('Reference not found') end context 'Validates variables' do diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 77c07b71c68..7d960dc411f 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -3,19 +3,26 @@ require 'spec_helper' describe Ci::CreatePipelineService, :services do let(:project) { create(:project, :repository) } let(:user) { create(:admin) } + let(:ref_name) { 'refs/heads/master' } before do stub_ci_pipeline_to_return_yaml_file end describe '#execute' do - def execute_service(source: :push, after: project.commit.id, message: 'Message', ref: 'refs/heads/master') + def execute_service( + source: :push, + after: project.commit.id, + message: 'Message', + ref: ref_name, + trigger_request: nil) params = { ref: ref, before: '00000000', after: after, commits: [{ message: message }] } - described_class.new(project, user, params).execute(source) + described_class.new(project, user, params).execute( + source, trigger_request: trigger_request) end context 'valid params' do @@ -320,5 +327,109 @@ describe Ci::CreatePipelineService, :services do end.not_to change { Environment.count } end end + + shared_examples 'when ref is protected' do + let(:user) { create(:user) } + + context 'when user is developer' do + before do + project.add_developer(user) + end + + it 'does not create a pipeline' do + expect(execute_service).not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + end + + context 'when user is master' do + before do + project.add_master(user) + end + + it 'creates a pipeline' do + expect(execute_service).to be_persisted + expect(Ci::Pipeline.count).to eq(1) + end + end + + context 'when trigger belongs to no one' do + let(:user) {} + let(:trigger_request) { create(:ci_trigger_request) } + + it 'does not create a pipeline' do + expect(execute_service(trigger_request: trigger_request)) + .not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + end + + context 'when trigger belongs to a developer' do + let(:user) {} + + let(:trigger_request) do + create(:ci_trigger_request).tap do |request| + user = create(:user) + project.add_developer(user) + request.trigger.update(owner: user) + end + end + + it 'does not create a pipeline' do + expect(execute_service(trigger_request: trigger_request)) + .not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + end + + context 'when trigger belongs to a master' do + let(:user) {} + + let(:trigger_request) do + create(:ci_trigger_request).tap do |request| + user = create(:user) + project.add_master(user) + request.trigger.update(owner: user) + end + end + + it 'does not create a pipeline' do + expect(execute_service(trigger_request: trigger_request)) + .to be_persisted + expect(Ci::Pipeline.count).to eq(1) + end + end + end + + context 'when ref is a protected branch' do + before do + create(:protected_branch, project: project, name: 'master') + end + + it_behaves_like 'when ref is protected' + end + + context 'when ref is a protected tag' do + let(:ref_name) { 'refs/tags/v1.0.0' } + + before do + create(:protected_tag, project: project, name: '*') + end + + it_behaves_like 'when ref is protected' + end + + context 'when ref is not protected' do + context 'when trigger belongs to no one' do + let(:user) {} + let(:trigger_request) { create(:ci_trigger_request) } + + it 'creates a pipeline' do + expect(execute_service(trigger_request: trigger_request)) + .to be_persisted + expect(Ci::Pipeline.count).to eq(1) + end + end + end end end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index f2956262f4b..8582c74e734 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -3,10 +3,13 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService, services: true do let(:service) { described_class.new } let(:project) { create(:project, :repository) } - let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger) { create(:ci_trigger, project: project, owner: owner) } + let(:owner) { create(:user) } before do stub_ci_pipeline_to_return_yaml_file + + project.add_developer(owner) end describe '#execute' do @@ -21,9 +24,6 @@ describe Ci::CreateTriggerRequestService, services: true do end context 'with owner' do - let(:owner) { create(:user) } - let(:trigger) { create(:ci_trigger, project: project, owner: owner) } - it { expect(subject).to be_kind_of(Ci::TriggerRequest) } it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } it { expect(subject.pipeline).to be_trigger } @@ -36,7 +36,7 @@ describe Ci::CreateTriggerRequestService, services: true do context 'no commit for ref' do subject { service.execute(project, trigger, 'other-branch') } - it { expect(subject).to be_nil } + it { expect(subject.pipeline).not_to be_persisted } end context 'no builds created' do @@ -46,7 +46,7 @@ describe Ci::CreateTriggerRequestService, services: true do stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }') end - it { expect(subject).to be_nil } + it { expect(subject.pipeline).not_to be_persisted } end end end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index a8f4bb72acf..74a9f90195c 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -74,6 +74,7 @@ describe PostReceive do OpenStruct.new(id: '123456') end allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true) + allow_any_instance_of(Repository).to receive(:ref_exists?).and_return(true) stub_ci_pipeline_to_return_yaml_file end |