From 9ecb85a4f36669fa05c961eef84cf46d7bf7f39c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 5 Jun 2017 23:38:06 +0800 Subject: [PATCH 001/143] Forbid creating pipeline if it's protected and cannot create the tag if it's a tag, and cannot merge the branch if it's a branch. --- app/services/ci/create_pipeline_service.rb | 10 ++++ .../ci/create_pipeline_service_spec.rb | 47 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 13baa63220d..a54af4749ac 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,6 +27,12 @@ module Ci return error('Reference not found') end + if tag? + return error("#{ref} is protected") unless access.can_create_tag?(ref) + else + return error("#{ref} is protected") unless access.can_merge_to_branch?(ref) + end + unless commit return error('Commit not found') end @@ -94,6 +100,10 @@ module Ci @commit ||= project.commit(origin_sha || origin_ref) end + def access + @access ||= Gitlab::UserAccess.new(current_user, project: project) + end + def sha commit.try(:id) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 597c3947e71..13a1c6a504d 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -3,13 +3,14 @@ require 'spec_helper' describe Ci::CreatePipelineService, services: true 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) params = { ref: ref, before: '00000000', after: after, @@ -311,5 +312,49 @@ describe Ci::CreatePipelineService, services: true 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 + 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 end end From 4408da47b8462055612548b8d43a679c861595e8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Jun 2017 00:56:38 +0800 Subject: [PATCH 002/143] Move the check to Pipeline.allowed_to_create? So that we could use it for the schedule before trying to use CreatePipelineService --- app/models/ci/pipeline.rb | 14 ++++ app/models/ci/pipeline_schedule.rb | 2 +- app/services/ci/create_pipeline_service.rb | 28 ++++--- spec/models/ci/pipeline_spec.rb | 97 ++++++++++++++++++++++ 4 files changed, 128 insertions(+), 13 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 425ca9278eb..e2caeda2289 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -162,6 +162,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_merge_to_branch?(ref) + elsif repo.ref_exists?("#{Gitlab::Git::TAG_REF_PREFIX}#{ref}") + access.can_create_tag?(ref) + else + false + end + end + def stage(name) stage = Ci::Stage.new(self, name: name) stage unless stage.statuses_count.zero? 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/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index a54af4749ac..5ed9d1aa517 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,10 +27,8 @@ module Ci return error('Reference not found') end - if tag? - return error("#{ref} is protected") unless access.can_create_tag?(ref) - else - return error("#{ref} is protected") unless access.can_merge_to_branch?(ref) + unless Ci::Pipeline.allowed_to_create?(current_user, project, ref) + return error("Insufficient permissions for protected #{ref}") end unless commit @@ -53,6 +51,12 @@ module Ci return error('No builds for this pipeline.') end + process! + end + + private + + def process! Ci::Pipeline.transaction do update_merge_requests_head_pipeline if pipeline.save @@ -66,8 +70,6 @@ module Ci pipeline.tap(&:process!) end - private - def update_merge_requests_head_pipeline return unless pipeline.latest? @@ -100,10 +102,6 @@ module Ci @commit ||= project.commit(origin_sha || origin_ref) end - def access - @access ||= Gitlab::UserAccess.new(current_user, project: project) - end - def sha commit.try(:id) end @@ -121,11 +119,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/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ae1b01b76ab..72af8130481 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 From 3c71c12b74ddc5875da2a4b53f0abd066a5a2f56 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Jun 2017 00:58:58 +0800 Subject: [PATCH 003/143] Add changelog entry --- changelogs/unreleased/30634-protected-pipeline.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/30634-protected-pipeline.yml 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: From 47b93fd76138ce24ec78926647497e52c5101dd8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Jun 2017 02:19:47 +0800 Subject: [PATCH 004/143] Don't check permission, only protected ref if no user --- app/services/ci/create_pipeline_service.rb | 10 +++- .../ci/create_pipeline_service_spec.rb | 57 ++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 5ed9d1aa517..7efea564ba6 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,7 +27,7 @@ module Ci return error('Reference not found') end - unless Ci::Pipeline.allowed_to_create?(current_user, project, ref) + unless triggering_user_allowed_for_ref?(trigger_request, ref) return error("Insufficient permissions for protected #{ref}") end @@ -56,6 +56,14 @@ module Ci 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 diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 13a1c6a504d..2616dcc6f04 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -10,13 +10,19 @@ describe Ci::CreatePipelineService, services: true do end describe '#execute' do - def execute_service(source: :push, after: project.commit.id, message: 'Message', ref: ref_name) + 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 @@ -337,6 +343,53 @@ describe Ci::CreatePipelineService, services: true do 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 From 9984f07a28273035d6c989913cb76c9c371965d0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Jun 2017 18:00:34 +0800 Subject: [PATCH 005/143] Disallow legacy trigger without a owner Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11910#note_31594492 https://gitlab.com/gitlab-org/gitlab-ce/issues/30634#note_31601001 --- app/services/ci/create_pipeline_service.rb | 8 +++++--- spec/services/ci/create_pipeline_service_spec.rb | 13 +++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 7efea564ba6..a51c52b3f91 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -23,6 +23,10 @@ module Ci return error('Insufficient permissions to create a new pipeline') end + unless trigger_request && trigger_request.trigger.owner + return error('Legacy trigger without a owner is not allowed') + end + unless branch? || tag? return error('Reference not found') end @@ -59,9 +63,7 @@ module Ci 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) + Ci::Pipeline.allowed_to_create?(triggering_user, project, ref) end def process! diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 2616dcc6f04..b8534a9d1aa 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -409,5 +409,18 @@ describe Ci::CreatePipelineService, services: true do 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 '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 + end end end From e86e1e515a7a4e4e1ee53d3d33bdfebfddd226a6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Jun 2017 20:23:19 +0800 Subject: [PATCH 006/143] Try to report why it's failing and fix tests --- app/services/ci/create_pipeline_service.rb | 2 +- app/services/ci/create_trigger_request_service.rb | 3 ++- lib/api/triggers.rb | 9 +++++---- lib/api/v3/triggers.rb | 7 ++++--- lib/ci/api/triggers.rb | 7 ++++--- spec/requests/ci/api/triggers_spec.rb | 14 ++++++++++++-- .../ci/create_trigger_request_service_spec.rb | 12 ++++++------ spec/workers/post_receive_spec.rb | 1 + 8 files changed, 35 insertions(+), 20 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index a51c52b3f91..b3dbb548454 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -23,7 +23,7 @@ module Ci return error('Insufficient permissions to create a new pipeline') end - unless trigger_request && trigger_request.trigger.owner + if trigger_request && !trigger_request.trigger.owner return error('Legacy trigger without a owner is not allowed') end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index beb27a5a597..e4f55c27f61 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/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/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_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 f4bc63bcc6a..7da48647bb5 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -82,6 +82,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 From 6d17ddac5aaf6c178a13c1e371b072780e7fd049 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Jun 2017 23:52:57 +0800 Subject: [PATCH 007/143] Still allow legacy triggers, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11910#note_31632911 --- app/services/ci/create_pipeline_service.rb | 8 +++----- spec/services/ci/create_pipeline_service_spec.rb | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index b3dbb548454..7efea564ba6 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -23,10 +23,6 @@ module Ci return error('Insufficient permissions to create a new pipeline') end - if trigger_request && !trigger_request.trigger.owner - return error('Legacy trigger without a owner is not allowed') - end - unless branch? || tag? return error('Reference not found') end @@ -63,7 +59,9 @@ module Ci def triggering_user_allowed_for_ref?(trigger_request, ref) triggering_user = current_user || trigger_request.trigger.owner - Ci::Pipeline.allowed_to_create?(triggering_user, project, ref) + (triggering_user && + Ci::Pipeline.allowed_to_create?(triggering_user, project, ref)) || + !project.protected_for?(ref) end def process! diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index b8534a9d1aa..348a0ab5102 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -415,10 +415,10 @@ describe Ci::CreatePipelineService, services: true do let(:user) {} let(:trigger_request) { create(:ci_trigger_request) } - it 'does not create a pipeline' do + it 'creates a pipeline' do expect(execute_service(trigger_request: trigger_request)) - .not_to be_persisted - expect(Ci::Pipeline.count).to eq(0) + .to be_persisted + expect(Ci::Pipeline.count).to eq(1) end end end From 25f930fbb34f285c2c4bde97c1e85d57a9e771d3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 7 Jun 2017 00:25:39 +0800 Subject: [PATCH 008/143] Fix other tests which tested against error message --- spec/requests/api/triggers_spec.rb | 3 ++- spec/requests/api/v3/triggers_spec.rb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 From 6e90bae1e7aa3f45089be58a2a59353a46c40493 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Mon, 13 Feb 2017 11:01:58 +0000 Subject: [PATCH 009/143] added .nvmrc --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000000..986084f369c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +7.1 \ No newline at end of file From d39b5085043b43ca76e8e91f7d114a468a004f0f Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 22 Jun 2017 11:20:16 +0000 Subject: [PATCH 010/143] Update .nvmrc to 7.5 --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 986084f369c..72906051c5c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -7.1 \ No newline at end of file +7.5 \ No newline at end of file From 23bfd8c13c803f4efdb9eaf8e6e3c1ffd17640e8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 4 Jul 2017 05:01:05 +0800 Subject: [PATCH 011/143] Consistently check permission for creating pipelines, updating builds and updating pipelines. We check against being able to merge or push if the ref is protected. --- app/models/ci/pipeline.rb | 2 +- app/policies/ci/build_policy.rb | 11 ++--- app/policies/ci/pipeline_policy.rb | 19 ++++++++- lib/gitlab/user_access.rb | 4 ++ spec/policies/ci/build_policy_spec.rb | 52 ++++++++---------------- spec/policies/ci/pipeline_policy_spec.rb | 47 +++++++++++++++++++++ 6 files changed, 93 insertions(+), 42 deletions(-) create mode 100644 spec/policies/ci/pipeline_policy_spec.rb diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index a46c1304667..06ce01095ea 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -169,7 +169,7 @@ module Ci Ability.allowed?(user, :create_pipeline, project) && if repo.ref_exists?("#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref}") - access.can_merge_to_branch?(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 diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 2d7405dc240..85245528602 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -11,19 +11,20 @@ module Ci cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" end - if can?(:update_build) && protected_action? + if can?(:update_build) && !can_user_update? cannot! :update_build end end private - def protected_action? - return false unless build.action? + def can_user_update? + user_access.can_push_or_merge_to_branch?(build.ref) + end - !::Gitlab::UserAccess + def user_access + @user_access ||= ::Gitlab::UserAccess .new(user, project: build.project) - .can_merge_to_branch?(build.ref) end end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 10aa2d3e72a..e71cc358353 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -1,7 +1,24 @@ module Ci class PipelinePolicy < BasePolicy + alias_method :pipeline, :subject + def rules - delegate! @subject.project + delegate! pipeline.project + + if can?(:update_pipeline) && !can_user_update? + cannot! :update_pipeline + end + end + + private + + def can_user_update? + user_access.can_push_or_merge_to_branch?(pipeline.ref) + end + + def user_access + @user_access ||= ::Gitlab::UserAccess + .new(user, project: pipeline.project) 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/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 48a139d4b83..b4c6f3141fb 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) + + create(:protected_branch, branch_policy, + name: build.ref, project: project) 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 no one can push or merge to the branch' do + let(:branch_policy) { :no_one_can_push } - 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(policies).not_to include :update_build - end - end - - context 'when build is not a manual action' do - let(:build) do - create(:ci_build, ref: 'some-ref', pipeline: pipeline) - end - - it 'includes ability to update build' do - expect(policies).to include :update_build - end + it 'does not include ability to update build' do + expect(policies).not_to include :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(policies).to include :update_build - end + it 'includes ability to update build' do + expect(policies).to include :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(policies).to include :update_build - end + it 'includes ability to update build' do + expect(policies).to include :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..4ecf07a1bf2 --- /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).not_to include :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 include :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 include :update_pipeline + end + end + end + end +end From 005870d5ce1a00b3405d0ae3a639d0c4befcb7a2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 4 Jul 2017 05:20:44 +0800 Subject: [PATCH 012/143] Fix bad conflict resolution --- app/policies/ci/pipeline_policy.rb | 2 +- app/services/ci/create_pipeline_service.rb | 22 ++++++++++++---------- spec/policies/ci/build_policy_spec.rb | 6 +++--- spec/policies/ci/pipeline_policy_spec.rb | 10 +++++----- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 73b5a40c7fc..8dba28b8d97 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -1,6 +1,6 @@ module Ci class PipelinePolicy < BasePolicy - delegate { pipeline.project } + delegate { @subject.project } condition(:user_cannot_update) do !::Gitlab::UserAccess diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index db12116b3ae..e487b7d5f30 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -51,19 +51,13 @@ module Ci return error('No stages / jobs for this pipeline.') end - process! + process! do + pipeline_created_counter.increment(source: source) + end 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 @@ -75,11 +69,19 @@ module Ci cancel_pending_pipelines if project.auto_cancel_pending_pipelines? - pipeline_created_counter.increment(source: source) + yield pipeline.tap(&:process!) end + 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 update_merge_requests_head_pipeline return unless pipeline.latest? diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 2a8e6653eb8..9e2b0506bf3 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -110,7 +110,7 @@ describe Ci::BuildPolicy, :models do let(:branch_policy) { :no_one_can_push } it 'does not include ability to update build' do - expect(policies).to be_disallowed :update_build + expect(policy).to be_disallowed :update_build end end @@ -118,7 +118,7 @@ describe Ci::BuildPolicy, :models do let(:branch_policy) { :developers_can_push } it 'includes ability to update build' do - expect(policies).to be_allowed :update_build + expect(policy).to be_allowed :update_build end end @@ -126,7 +126,7 @@ describe Ci::BuildPolicy, :models do let(:branch_policy) { :developers_can_merge } it 'includes ability to update build' do - expect(policies).to be_allowed :update_build + expect(policy).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 index db09be96875..cc04230411f 100644 --- a/spec/policies/ci/pipeline_policy_spec.rb +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -4,8 +4,8 @@ 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 + let(:policy) do + described_class.new(user, pipeline) end describe 'rules' do @@ -23,7 +23,7 @@ describe Ci::PipelinePolicy, :models do let(:branch_policy) { :no_one_can_push } it 'does not include ability to update pipeline' do - expect(policies).to be_disallowed :update_pipeline + expect(policy).to be_disallowed :update_pipeline end end @@ -31,7 +31,7 @@ describe Ci::PipelinePolicy, :models do let(:branch_policy) { :developers_can_push } it 'includes ability to update pipeline' do - expect(policies).to be_allowed :update_pipeline + expect(policy).to be_allowed :update_pipeline end end @@ -39,7 +39,7 @@ describe Ci::PipelinePolicy, :models do let(:branch_policy) { :developers_can_merge } it 'includes ability to update pipeline' do - expect(policies).to be_allowed :update_pipeline + expect(policy).to be_allowed :update_pipeline end end end From 28553dbc05989b698777ee085aa2a357ffe576d2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 4 Jul 2017 05:58:28 +0800 Subject: [PATCH 013/143] Update tests due to permission changes --- .../projects/jobs_controller_spec.rb | 10 ++++---- .../projects/pipelines_controller_spec.rb | 8 +++--- .../gitlab/ci/status/build/cancelable_spec.rb | 2 +- .../gitlab/ci/status/build/factory_spec.rb | 25 +++++++++++++------ .../gitlab/ci/status/build/retryable_spec.rb | 2 +- spec/lib/gitlab/ci/status/build/stop_spec.rb | 2 +- spec/models/ci/pipeline_spec.rb | 4 +-- spec/serializers/job_entity_spec.rb | 6 ++++- .../pipeline_details_entity_spec.rb | 6 ++--- spec/serializers/pipeline_entity_spec.rb | 4 +-- .../ci/process_pipeline_service_spec.rb | 2 +- spec/services/ci/retry_build_service_spec.rb | 4 +-- .../ci/retry_pipeline_service_spec.rb | 20 +++++---------- .../create_deployment_service_spec.rb | 2 +- 14 files changed, 51 insertions(+), 46 deletions(-) diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 472e5fc51a0..9ed48d98360 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -218,7 +218,7 @@ describe Projects::JobsController do describe 'POST retry' do before do - project.add_developer(user) + project.add_master(user) sign_in(user) post_retry @@ -250,7 +250,7 @@ describe Projects::JobsController do describe 'POST play' do before do - project.add_developer(user) + project.add_master(user) create(:protected_branch, :developers_can_merge, name: 'master', project: project) @@ -290,7 +290,7 @@ describe Projects::JobsController do describe 'POST cancel' do before do - project.add_developer(user) + project.add_master(user) sign_in(user) post_cancel @@ -326,7 +326,7 @@ describe Projects::JobsController do describe 'POST cancel_all' do before do - project.add_developer(user) + project.add_master(user) sign_in(user) end @@ -368,7 +368,7 @@ describe Projects::JobsController do describe 'POST erase' do before do - project.add_developer(user) + project.add_master(user) sign_in(user) post_erase diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 734532668d3..3b4d7d069c9 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -8,7 +8,7 @@ describe Projects::PipelinesController do let(:feature) { ProjectFeature::DISABLED } before do - project.add_developer(user) + project.add_master(user) project.project_feature.update( builds_access_level: feature) @@ -158,7 +158,7 @@ describe Projects::PipelinesController do context 'when builds are enabled' do let(:feature) { ProjectFeature::ENABLED } - + it 'retries a pipeline without returning any content' do expect(response).to have_http_status(:no_content) expect(build.reload).to be_retried @@ -175,7 +175,7 @@ describe Projects::PipelinesController do describe 'POST cancel.json' do let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:build) { create(:ci_build, :running, pipeline: pipeline) } - + before do post :cancel, namespace_id: project.namespace, project_id: project, @@ -185,7 +185,7 @@ describe Projects::PipelinesController do context 'when builds are enabled' do let(:feature) { ProjectFeature::ENABLED } - + it 'cancels a pipeline without returning any content' do expect(response).to have_http_status(:no_content) expect(pipeline.reload).to be_canceled diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index 114d2490490..e7b880c9b09 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -48,7 +48,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do describe '#has_action?' do context 'when user is allowed to update build' do before do - build.project.team << [user, :developer] + build.project.add_master(user) end it { is_expected.to have_action } diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index c8a97016f20..bc21b8af67c 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Ci::Status::Build::Factory do let(:factory) { described_class.new(build, user) } before do - project.team << [user, :developer] + project.add_master(user) end context 'when build is successful' do @@ -225,19 +225,20 @@ describe Gitlab::Ci::Status::Build::Factory do end context 'when user has ability to play action' do - before do - project.add_developer(user) - - create(:protected_branch, :developers_can_merge, - name: build.ref, project: project) - end - it 'fabricates status that has action' do expect(status).to have_action end end context 'when user does not have ability to play action' do + before do + project.team.truncate + project.add_developer(user) + + create(:protected_branch, :no_one_can_push, + name: build.ref, project: project) + end + it 'fabricates status that has no action' do expect(status).not_to have_action end @@ -262,6 +263,14 @@ describe Gitlab::Ci::Status::Build::Factory do end context 'when user is not allowed to execute manual action' do + before do + project.team.truncate + project.add_developer(user) + + create(:protected_branch, :no_one_can_push, + name: build.ref, project: project) + end + it 'fabricates status with correct details' do expect(status.text).to eq 'manual' expect(status.group).to eq 'manual' diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index 099d873fc01..ed9752b4ed6 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -48,7 +48,7 @@ describe Gitlab::Ci::Status::Build::Retryable do describe '#has_action?' do context 'when user is allowed to update build' do before do - build.project.team << [user, :developer] + build.project.add_master(user) end it { is_expected.to have_action } diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 23902f26b1a..7fe3cf7ea6d 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Ci::Status::Build::Stop do describe '#has_action?' do context 'when user is allowed to update build' do before do - build.project.team << [user, :developer] + build.project.add_master(user) end it { is_expected.to have_action } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 776a674a6d9..7463fb3d379 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -832,7 +832,7 @@ describe Ci::Pipeline, models: true do context 'on failure and build retry' do before do build.drop - project.add_developer(user) + project.add_master(user) Ci::Build.retry(build, user) end @@ -1063,7 +1063,7 @@ describe Ci::Pipeline, models: true do let(:latest_status) { pipeline.statuses.latest.pluck(:status) } before do - project.add_developer(user) + project.add_master(user) end context 'when there is a failed build and failed external status' do diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index 5ca7bf2fcaf..ec30816654b 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -8,7 +8,7 @@ describe JobEntity do before do allow(request).to receive(:current_user).and_return(user) - project.add_developer(user) + project.add_master(user) end let(:entity) do @@ -90,6 +90,10 @@ describe JobEntity do end context 'when user is not allowed to trigger action' do + before do + project.team.truncate + end + it 'does not contain path to play action' do expect(subject).not_to include(:play_path) end diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb index d28dec9592a..e9b24b47900 100644 --- a/spec/serializers/pipeline_details_entity_spec.rb +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -52,7 +52,7 @@ describe PipelineDetailsEntity do context 'user has ability to retry pipeline' do before do - project.team << [user, :developer] + project.add_master(user) end it 'retryable flag is true' do @@ -80,7 +80,7 @@ describe PipelineDetailsEntity do context 'user has ability to cancel pipeline' do before do - project.add_developer(user) + project.add_master(user) end it 'cancelable flag is true' do @@ -97,7 +97,7 @@ describe PipelineDetailsEntity do context 'when pipeline has commit statuses' do let(:pipeline) { create(:ci_empty_pipeline) } - + before do create(:generic_commit_status, pipeline: pipeline) end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index 46650f3a80d..46433867b11 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -52,7 +52,7 @@ describe PipelineEntity do context 'user has ability to retry pipeline' do before do - project.team << [user, :developer] + project.add_master(user) end it 'contains retry path' do @@ -80,7 +80,7 @@ describe PipelineEntity do context 'user has ability to cancel pipeline' do before do - project.add_developer(user) + project.add_master(user) end it 'contains cancel path' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index efcaccc254e..1e938a97f5a 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -9,7 +9,7 @@ describe Ci::ProcessPipelineService, '#execute', :services do end before do - project.add_developer(user) + project.add_master(user) end context 'when simple pipeline is defined' do diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index ef9927c5969..52c6a4a0bc8 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -85,7 +85,7 @@ describe Ci::RetryBuildService, :services do context 'when user has ability to execute build' do before do - project.add_developer(user) + project.add_master(user) end it_behaves_like 'build duplication' @@ -131,7 +131,7 @@ describe Ci::RetryBuildService, :services do context 'when user has ability to execute build' do before do - project.add_developer(user) + project.add_master(user) end it_behaves_like 'build duplication' diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index 3e860203063..7798db3f3b9 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -244,13 +244,9 @@ describe Ci::RetryPipelineService, '#execute', :services do create_build('verify', :canceled, 1) end - it 'does not reprocess manual action' do - service.execute(pipeline) - - expect(build('test')).to be_pending - expect(build('deploy')).to be_failed - expect(build('verify')).to be_created - expect(pipeline.reload).to be_running + it 'raises an error' do + expect { service.execute(pipeline) } + .to raise_error Gitlab::Access::AccessDeniedError end end @@ -261,13 +257,9 @@ describe Ci::RetryPipelineService, '#execute', :services do create_build('verify', :canceled, 2) end - it 'does not reprocess manual action' do - service.execute(pipeline) - - expect(build('test')).to be_pending - expect(build('deploy')).to be_failed - expect(build('verify')).to be_created - expect(pipeline.reload).to be_running + it 'raises an error' do + expect { service.execute(pipeline) } + .to raise_error Gitlab::Access::AccessDeniedError end end end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index dfab6ebf372..844d9d63428 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -244,7 +244,7 @@ describe CreateDeploymentService, services: true do context 'when job is retried' do it_behaves_like 'creates deployment' do before do - project.add_developer(user) + project.add_master(user) end let(:deployable) { Ci::Build.retry(job, user) } From 216bf78fd154005cbf8ec447bfa23f77f6b26775 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 4 Jul 2017 17:48:45 +0800 Subject: [PATCH 014/143] Introduce Gitlab::Cache::RequestStoreWrap So that we cache the result of UserAccess#can_push_or_merge_to_branch? in RequestStore, avoiding querying ProtectedBranch over and over for the list of pipelines (i.e. in PipelineSerializer) I don't think this is ideal because I don't like the idea of RequestStore in general, but this is the easiest way to cache it without changing the architecture. In the future we should cache more explicitly rather than this kind of global store. --- lib/gitlab/cache/request_store_wrap.rb | 60 ++++++++++++++++++++ lib/gitlab/user_access.rb | 10 +++- spec/serializers/pipeline_serializer_spec.rb | 2 +- 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 lib/gitlab/cache/request_store_wrap.rb diff --git a/lib/gitlab/cache/request_store_wrap.rb b/lib/gitlab/cache/request_store_wrap.rb new file mode 100644 index 00000000000..3e0a5f06b53 --- /dev/null +++ b/lib/gitlab/cache/request_store_wrap.rb @@ -0,0 +1,60 @@ +module Gitlab + module Cache + # This module provides a simple way to cache values in RequestStore, + # and the cache key would be based on the class name, method name, + # customized instance level values, and arguments. + # + # A simple example: + # + # class UserAccess + # extend Gitlab::Cache::RequestStoreWrap + # + # request_store_wrap_key do + # [user.id, project.id] + # end + # + # request_store_wrap def can_push_to_branch?(ref) + # # ... + # end + # end + # + # This way, the result of `can_push_to_branch?` would be cached in + # `RequestStore.store` based on the cache key. + module RequestStoreWrap + def self.extended(klass) + return if klass < self + + extension = Module.new + klass.const_set(:RequestStoreWrapExtension, extension) + klass.prepend(extension) + end + + def request_store_wrap_key(&block) + if block_given? + @request_store_wrap_key = block + else + @request_store_wrap_key + end + end + + def request_store_wrap(method_name) + const_get(:RequestStoreWrapExtension) + .send(:define_method, method_name) do |*args| + return super(*args) unless RequestStore.active? + + klass = self.class + key = [klass.name, + method_name, + *instance_exec(&klass.request_store_wrap_key), + *args].join(':') + + if RequestStore.store.key?(key) + RequestStore.store[key] + else + RequestStore.store[key] = super(*args) + end + end + end + end + end +end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index bb05c474fa2..d8b043f5021 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -1,5 +1,11 @@ module Gitlab class UserAccess + extend Gitlab::Cache::RequestStoreWrap + + request_store_wrap_key do + [user&.id, project&.id] + end + attr_reader :user, :project def initialize(user, project: nil) @@ -52,7 +58,7 @@ module Gitlab can_push_to_branch?(ref) || can_merge_to_branch?(ref) end - def can_push_to_branch?(ref) + request_store_wrap def can_push_to_branch?(ref) return false unless can_access_git? if ProtectedBranch.protected?(project, ref) @@ -64,7 +70,7 @@ module Gitlab end end - def can_merge_to_branch?(ref) + request_store_wrap def can_merge_to_branch?(ref) return false unless can_access_git? if ProtectedBranch.protected?(project, ref) diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 44813656aff..8dc666586c7 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -110,7 +110,7 @@ describe PipelineSerializer do it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.count).to be_within(1).of(57) + expect(recorded.count).to be_within(1).of(59) expect(recorded.cached_count).to eq(0) end From a4dd3ea168d19d2b65b7e55ed0043c7e7dcac77c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 4 Jul 2017 18:00:39 +0800 Subject: [PATCH 015/143] Make sure that retryable_builds would preload project --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bea2ec1e18c..7963386bdb1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -21,7 +21,7 @@ module Ci has_many :merge_requests, foreign_key: "head_pipeline_id" has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build' - has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build' + has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus' has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' From 090f034b480b8e8b6dee87765878d1746cc75bce Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 4 Jul 2017 22:31:11 +0800 Subject: [PATCH 016/143] Add test for RequestStoreWrap --- .../gitlab/cache/request_store_wrap_spec.rb | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 spec/lib/gitlab/cache/request_store_wrap_spec.rb diff --git a/spec/lib/gitlab/cache/request_store_wrap_spec.rb b/spec/lib/gitlab/cache/request_store_wrap_spec.rb new file mode 100644 index 00000000000..82b47c3c7ae --- /dev/null +++ b/spec/lib/gitlab/cache/request_store_wrap_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe Gitlab::Cache::RequestStoreWrap, :request_store do + class ExpensiveAlgorithm < Struct.new(:id, :name, :result) + extend Gitlab::Cache::RequestStoreWrap + + request_store_wrap_key do + [id, name] + end + + request_store_wrap def compute(arg) + result << arg + end + + request_store_wrap def repute(arg) + result << arg + end + end + + let(:algorithm) { ExpensiveAlgorithm.new('id', 'name', []) } + + context 'when RequestStore is active' do + it 'does not compute twice for the same argument' do + result = algorithm.compute(true) + + expect(result).to eq([true]) + expect(algorithm.compute(true)).to eq(result) + expect(algorithm.result).to eq(result) + end + + it 'computes twice for the different argument' do + algorithm.compute(true) + result = algorithm.compute(false) + + expect(result).to eq([true, false]) + expect(algorithm.result).to eq(result) + end + + it 'computes twice for the different keys, id' do + algorithm.compute(true) + algorithm.id = 'ad' + result = algorithm.compute(true) + + expect(result).to eq([true, true]) + expect(algorithm.result).to eq(result) + end + + it 'computes twice for the different keys, name' do + algorithm.compute(true) + algorithm.name = 'same' + result = algorithm.compute(true) + + expect(result).to eq([true, true]) + expect(algorithm.result).to eq(result) + end + + it 'computes twice for the different class name' do + algorithm.compute(true) + allow(ExpensiveAlgorithm).to receive(:name).and_return('CheapAlgo') + result = algorithm.compute(true) + + expect(result).to eq([true, true]) + expect(algorithm.result).to eq(result) + end + + it 'computes twice for the different method' do + algorithm.compute(true) + result = algorithm.repute(true) + + expect(result).to eq([true, true]) + expect(algorithm.result).to eq(result) + end + + it 'computes twice if RequestStore starts over' do + algorithm.compute(true) + RequestStore.end! + RequestStore.clear! + RequestStore.begin! + result = algorithm.compute(true) + + expect(result).to eq([true, true]) + expect(algorithm.result).to eq(result) + end + end + + context 'when RequestStore is inactive' do + before do + RequestStore.end! + end + + it 'computes twice even if everything is the same' do + algorithm.compute(true) + result = algorithm.compute(true) + + expect(result).to eq([true, true]) + expect(algorithm.result).to eq(result) + end + end +end From 2afa90b64a01eaefafacabb1f048835858ece15c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 4 Jul 2017 23:28:07 +0800 Subject: [PATCH 017/143] Don't extend from struct as rubocop suggests --- spec/lib/gitlab/cache/request_store_wrap_spec.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spec/lib/gitlab/cache/request_store_wrap_spec.rb b/spec/lib/gitlab/cache/request_store_wrap_spec.rb index 82b47c3c7ae..87ea26a9635 100644 --- a/spec/lib/gitlab/cache/request_store_wrap_spec.rb +++ b/spec/lib/gitlab/cache/request_store_wrap_spec.rb @@ -1,9 +1,17 @@ require 'spec_helper' describe Gitlab::Cache::RequestStoreWrap, :request_store do - class ExpensiveAlgorithm < Struct.new(:id, :name, :result) + class ExpensiveAlgorithm extend Gitlab::Cache::RequestStoreWrap + attr_accessor :id, :name, :result + + def initialize(id, name, result) + self.id = id + self.name = name + self.result = result + end + request_store_wrap_key do [id, name] end From 56ea7a0cfe0fcdff33de80fd4602f463367914b2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 5 Jul 2017 21:55:35 +0800 Subject: [PATCH 018/143] Merge allowed_to_create? into CreatePipelineService --- app/models/ci/pipeline.rb | 14 --- app/models/ci/pipeline_schedule.rb | 4 - app/services/ci/create_pipeline_service.rb | 22 +++- app/workers/pipeline_schedule_worker.rb | 13 +-- spec/models/ci/pipeline_spec.rb | 97 ----------------- .../ci/create_pipeline_service_spec.rb | 100 ++++++++++++++++++ 6 files changed, 122 insertions(+), 128 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 7963386bdb1..8d1beca9771 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -164,20 +164,6 @@ 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 eaca2774bf9..49455e79c15 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -36,10 +36,6 @@ module Ci update_attribute(:active, false) end - def runnable_by_owner? - Ci::Pipeline.allowed_to_create?(owner, project, ref) - end - def set_next_run_at self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index e487b7d5f30..485161e5f3f 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,7 +27,7 @@ module Ci return error('Reference not found') end - unless triggering_user_allowed_for_ref?(trigger_request, ref) + unless triggering_user_allowed_for_ref?(trigger_request) return error("Insufficient permissions for protected #{ref}") end @@ -74,14 +74,26 @@ module Ci pipeline.tap(&:process!) end - def triggering_user_allowed_for_ref?(trigger_request, ref) + def triggering_user_allowed_for_ref?(trigger_request) triggering_user = current_user || trigger_request.trigger.owner - (triggering_user && - Ci::Pipeline.allowed_to_create?(triggering_user, project, ref)) || + (triggering_user && allowed_to_create?(triggering_user)) || !project.protected_for?(ref) end + def allowed_to_create?(triggering_user) + access = Gitlab::UserAccess.new(triggering_user, project: project) + + Ability.allowed?(triggering_user, :create_pipeline, project) && + if branch? + access.can_push_or_merge_to_branch?(ref) + elsif tag? + access.can_create_tag?(ref) + else + false + end + end + def update_merge_requests_head_pipeline return unless pipeline.latest? @@ -145,7 +157,7 @@ module Ci end def ref - Gitlab::Git.ref_name(origin_ref) + @ref ||= Gitlab::Git.ref_name(origin_ref) end def valid_sha? diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb index 7b485b3363c..d7087f20dfc 100644 --- a/app/workers/pipeline_schedule_worker.rb +++ b/app/workers/pipeline_schedule_worker.rb @@ -6,15 +6,12 @@ class PipelineScheduleWorker Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) .preload(:owner, :project).find_each do |schedule| begin - unless schedule.runnable_by_owner? - schedule.deactivate! - next - end - - Ci::CreatePipelineService.new(schedule.project, - schedule.owner, - ref: schedule.ref) + pipeline = Ci::CreatePipelineService.new(schedule.project, + schedule.owner, + ref: schedule.ref) .execute(:schedule, save_on_errors: false, schedule: schedule) + + schedule.deactivate! unless pipeline.persisted? rescue => e Rails.logger.error "#{schedule.id}: Failed to create a scheduled pipeline: #{e.message}" ensure diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 7463fb3d379..d400bdfe8f8 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -28,103 +28,6 @@ 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/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 7d960dc411f..66218772084 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -432,4 +432,104 @@ describe Ci::CreatePipelineService, :services do end end end + + describe '#allowed_to_create?' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:ref) { 'master' } + + subject do + described_class.new(project, user, ref: ref) + .send(:allowed_to_create?, user) + end + + 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 end From 550ccf443059412a26adfcba15fbe9d05d39a5f9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 6 Jul 2017 17:37:27 +0800 Subject: [PATCH 019/143] Make message and code more clear --- app/services/ci/create_pipeline_service.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 485161e5f3f..a8034e30a85 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -28,7 +28,7 @@ module Ci end unless triggering_user_allowed_for_ref?(trigger_request) - return error("Insufficient permissions for protected #{ref}") + return error("Insufficient permissions for protected ref '#{ref}'") end unless commit @@ -77,8 +77,11 @@ module Ci def triggering_user_allowed_for_ref?(trigger_request) triggering_user = current_user || trigger_request.trigger.owner - (triggering_user && allowed_to_create?(triggering_user)) || + if triggering_user + allowed_to_create?(triggering_user) + else # legacy triggers don't have a corresponding user !project.protected_for?(ref) + end end def allowed_to_create?(triggering_user) From b8f2bc749fa9bc4b0b2ad0b02b56fc39fe12cffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Thu, 13 Jul 2017 09:54:28 +0800 Subject: [PATCH 020/143] Add uk translation difference of Pipeline Schedules --- locale/uk/part.po | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 locale/uk/part.po diff --git a/locale/uk/part.po b/locale/uk/part.po new file mode 100644 index 00000000000..ef5864be5c9 --- /dev/null +++ b/locale/uk/part.po @@ -0,0 +1,38 @@ +# Андрей Витюк , 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-07-12 07:29-0400\n" +"Last-Translator: Андрей Витюк \n" +"Language-Team: Ukrainian\n" +"Language: uk\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgid "PipelineSchedules|Input variable key" +msgstr "Введіть ім'я змінної" + +msgid "PipelineSchedules|Input variable value" +msgstr "Вхідні значення змінних" + +msgid "PipelineSchedules|Remove variable row" +msgstr "Видалити змінні" + +msgid "PipelineSchedules|Variables" +msgstr "Змінні" + +msgid "" +"You are going to remove %{group_name}.\n" +"Removed groups CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" +"Ви хочете видалити %{group_name}.\n" +"Видалені групи НЕ МОЖНА буду відновити!\n" +"Ви АБСОЛЮТНО впевнені?" + From 67f444471e67e2e9420424f1a79386df13bf3157 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Mon, 17 Jul 2017 22:26:48 +0900 Subject: [PATCH 021/143] Add link to doc/api/ci/lint.md --- changelogs/unreleased/35204-doc-api-ci-lint-typo.yml | 4 ++++ doc/api/ci/lint.md | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 changelogs/unreleased/35204-doc-api-ci-lint-typo.yml diff --git a/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml b/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml new file mode 100644 index 00000000000..45b6c57579b --- /dev/null +++ b/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml @@ -0,0 +1,4 @@ +--- +title: Add link to doc/api/ci/lint.md +merge_request: 12914 +author: Takuya Noguchi diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md index 6a4dca92cfe..e4a6dc809b1 100644 --- a/doc/api/ci/lint.md +++ b/doc/api/ci/lint.md @@ -47,3 +47,5 @@ Example responses: "error": "content is missing" } ``` + +[ce-5953]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5953 From 3c34a0b99be2cf858831043403ba2268ac270c77 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 18 Jul 2017 20:17:24 +0800 Subject: [PATCH 022/143] Remove old request store wrap --- lib/gitlab/cache/request_store_wrap.rb | 60 ---------- .../gitlab/cache/request_store_wrap_spec.rb | 107 ------------------ 2 files changed, 167 deletions(-) delete mode 100644 lib/gitlab/cache/request_store_wrap.rb delete mode 100644 spec/lib/gitlab/cache/request_store_wrap_spec.rb diff --git a/lib/gitlab/cache/request_store_wrap.rb b/lib/gitlab/cache/request_store_wrap.rb deleted file mode 100644 index 3e0a5f06b53..00000000000 --- a/lib/gitlab/cache/request_store_wrap.rb +++ /dev/null @@ -1,60 +0,0 @@ -module Gitlab - module Cache - # This module provides a simple way to cache values in RequestStore, - # and the cache key would be based on the class name, method name, - # customized instance level values, and arguments. - # - # A simple example: - # - # class UserAccess - # extend Gitlab::Cache::RequestStoreWrap - # - # request_store_wrap_key do - # [user.id, project.id] - # end - # - # request_store_wrap def can_push_to_branch?(ref) - # # ... - # end - # end - # - # This way, the result of `can_push_to_branch?` would be cached in - # `RequestStore.store` based on the cache key. - module RequestStoreWrap - def self.extended(klass) - return if klass < self - - extension = Module.new - klass.const_set(:RequestStoreWrapExtension, extension) - klass.prepend(extension) - end - - def request_store_wrap_key(&block) - if block_given? - @request_store_wrap_key = block - else - @request_store_wrap_key - end - end - - def request_store_wrap(method_name) - const_get(:RequestStoreWrapExtension) - .send(:define_method, method_name) do |*args| - return super(*args) unless RequestStore.active? - - klass = self.class - key = [klass.name, - method_name, - *instance_exec(&klass.request_store_wrap_key), - *args].join(':') - - if RequestStore.store.key?(key) - RequestStore.store[key] - else - RequestStore.store[key] = super(*args) - end - end - end - end - end -end diff --git a/spec/lib/gitlab/cache/request_store_wrap_spec.rb b/spec/lib/gitlab/cache/request_store_wrap_spec.rb deleted file mode 100644 index 87ea26a9635..00000000000 --- a/spec/lib/gitlab/cache/request_store_wrap_spec.rb +++ /dev/null @@ -1,107 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Cache::RequestStoreWrap, :request_store do - class ExpensiveAlgorithm - extend Gitlab::Cache::RequestStoreWrap - - attr_accessor :id, :name, :result - - def initialize(id, name, result) - self.id = id - self.name = name - self.result = result - end - - request_store_wrap_key do - [id, name] - end - - request_store_wrap def compute(arg) - result << arg - end - - request_store_wrap def repute(arg) - result << arg - end - end - - let(:algorithm) { ExpensiveAlgorithm.new('id', 'name', []) } - - context 'when RequestStore is active' do - it 'does not compute twice for the same argument' do - result = algorithm.compute(true) - - expect(result).to eq([true]) - expect(algorithm.compute(true)).to eq(result) - expect(algorithm.result).to eq(result) - end - - it 'computes twice for the different argument' do - algorithm.compute(true) - result = algorithm.compute(false) - - expect(result).to eq([true, false]) - expect(algorithm.result).to eq(result) - end - - it 'computes twice for the different keys, id' do - algorithm.compute(true) - algorithm.id = 'ad' - result = algorithm.compute(true) - - expect(result).to eq([true, true]) - expect(algorithm.result).to eq(result) - end - - it 'computes twice for the different keys, name' do - algorithm.compute(true) - algorithm.name = 'same' - result = algorithm.compute(true) - - expect(result).to eq([true, true]) - expect(algorithm.result).to eq(result) - end - - it 'computes twice for the different class name' do - algorithm.compute(true) - allow(ExpensiveAlgorithm).to receive(:name).and_return('CheapAlgo') - result = algorithm.compute(true) - - expect(result).to eq([true, true]) - expect(algorithm.result).to eq(result) - end - - it 'computes twice for the different method' do - algorithm.compute(true) - result = algorithm.repute(true) - - expect(result).to eq([true, true]) - expect(algorithm.result).to eq(result) - end - - it 'computes twice if RequestStore starts over' do - algorithm.compute(true) - RequestStore.end! - RequestStore.clear! - RequestStore.begin! - result = algorithm.compute(true) - - expect(result).to eq([true, true]) - expect(algorithm.result).to eq(result) - end - end - - context 'when RequestStore is inactive' do - before do - RequestStore.end! - end - - it 'computes twice even if everything is the same' do - algorithm.compute(true) - result = algorithm.compute(true) - - expect(result).to eq([true, true]) - expect(algorithm.result).to eq(result) - end - end -end From c86e74b284826e2f53bbcba763edd113a7022ffc Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 18 Jul 2017 21:08:48 +0800 Subject: [PATCH 023/143] Restore some tests from master --- spec/policies/ci/build_policy_spec.rb | 38 +++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 86e57fdf607..9041460ea91 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -98,16 +98,17 @@ describe Ci::BuildPolicy, :models do describe 'rules for protected branch' do let(:project) { create(:project) } + let(:build) { create(:ci_build, ref: 'some-ref', pipeline: pipeline) } before do project.add_developer(user) - - create(:protected_branch, branch_policy, - name: build.ref, project: project) end context 'when no one can push or merge to the branch' do - let(:branch_policy) { :no_one_can_push } + before do + create(:protected_branch, :no_one_can_push, + name: 'some-ref', project: project) + end it 'does not include ability to update build' do expect(policy).to be_disallowed :update_build @@ -115,7 +116,34 @@ describe Ci::BuildPolicy, :models do end context 'when developers can push to the branch' do - let(:branch_policy) { :developers_can_merge } + before do + create(:protected_branch, :developers_can_merge, + name: 'some-ref', project: project) + end + + it 'includes ability to update build' do + expect(policy).to be_allowed :update_build + end + end + + context 'when no one can create the tag' do + before do + create(:protected_tag, :no_one_can_create, + name: 'some-ref', project: project) + + build.update(tag: true) + end + + it 'does not include ability to update build' do + expect(policy).to be_disallowed :update_build + end + end + + context 'when no one can create the tag but it is not a tag' do + before do + create(:protected_tag, :no_one_can_create, + name: 'some-ref', project: project) + end it 'includes ability to update build' do expect(policy).to be_allowed :update_build From 679789ee93b0e5db3863bfcd539e20074c140984 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 18 Jul 2017 21:56:28 +0800 Subject: [PATCH 024/143] Rename can_push_or_merge_to_branch? to can_update_branch? Also make sure pipeline would also check against tag as well --- app/policies/ci/build_policy.rb | 2 +- app/policies/ci/pipeline_policy.rb | 10 +++++++--- app/services/ci/create_pipeline_service.rb | 2 +- lib/gitlab/user_access.rb | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 00adb51e7de..00f18d0155b 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -6,7 +6,7 @@ module Ci if @subject.tag? !access.can_create_tag?(@subject.ref) else - !access.can_push_or_merge_to_branch?(@subject.ref) + !access.can_update_branch?(@subject.ref) end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 8dba28b8d97..07d724c9cfd 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -3,9 +3,13 @@ module Ci delegate { @subject.project } condition(:user_cannot_update) do - !::Gitlab::UserAccess - .new(@user, project: @subject.project) - .can_push_or_merge_to_branch?(@subject.ref) + access = ::Gitlab::UserAccess.new(@user, project: @subject.project) + + if @subject.tag? + !access.can_create_tag?(@subject.ref) + else + !access.can_update_branch?(@subject.ref) + end end rule { user_cannot_update }.prevent :update_pipeline diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 8e2184a1f19..8b689968895 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -89,7 +89,7 @@ module Ci Ability.allowed?(triggering_user, :create_pipeline, project) && if branch? - access.can_push_or_merge_to_branch?(ref) + access.can_update_branch?(ref) elsif tag? access.can_create_tag?(ref) else diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index c63b98500ee..25698bb8e99 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -54,7 +54,7 @@ module Gitlab end end - def can_push_or_merge_to_branch?(ref) + def can_update_branch?(ref) can_push_to_branch?(ref) || can_merge_to_branch?(ref) end From a27cf281b17641f3f33712633099369867415309 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 18 Jul 2017 22:04:22 +0800 Subject: [PATCH 025/143] Unify build policy tests and pipeline policy tests --- spec/policies/ci/build_policy_spec.rb | 10 +++---- spec/policies/ci/pipeline_policy_spec.rb | 35 ++++++++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 9041460ea91..e3ea3c960a4 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -96,7 +96,7 @@ describe Ci::BuildPolicy, :models do end end - describe 'rules for protected branch' do + describe 'rules for protected ref' do let(:project) { create(:project) } let(:build) { create(:ci_build, ref: 'some-ref', pipeline: pipeline) } @@ -107,7 +107,7 @@ describe Ci::BuildPolicy, :models do context 'when no one can push or merge to the branch' do before do create(:protected_branch, :no_one_can_push, - name: 'some-ref', project: project) + name: build.ref, project: project) end it 'does not include ability to update build' do @@ -118,7 +118,7 @@ describe Ci::BuildPolicy, :models do context 'when developers can push to the branch' do before do create(:protected_branch, :developers_can_merge, - name: 'some-ref', project: project) + name: build.ref, project: project) end it 'includes ability to update build' do @@ -129,7 +129,7 @@ describe Ci::BuildPolicy, :models do context 'when no one can create the tag' do before do create(:protected_tag, :no_one_can_create, - name: 'some-ref', project: project) + name: build.ref, project: project) build.update(tag: true) end @@ -142,7 +142,7 @@ describe Ci::BuildPolicy, :models do context 'when no one can create the tag but it is not a tag' do before do create(:protected_tag, :no_one_can_create, - name: 'some-ref', project: project) + name: build.ref, project: project) end it 'includes ability to update build' do diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb index cc04230411f..b11b06d301f 100644 --- a/spec/policies/ci/pipeline_policy_spec.rb +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -9,18 +9,18 @@ describe Ci::PipelinePolicy, :models do end describe 'rules' do - describe 'rules for protected branch' do + describe 'rules for protected ref' 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 } + before do + create(:protected_branch, :no_one_can_push, + name: pipeline.ref, project: project) + end it 'does not include ability to update pipeline' do expect(policy).to be_disallowed :update_pipeline @@ -28,15 +28,34 @@ describe Ci::PipelinePolicy, :models do end context 'when developers can push to the branch' do - let(:branch_policy) { :developers_can_push } + before do + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) + end it 'includes ability to update pipeline' do expect(policy).to be_allowed :update_pipeline end end - context 'when developers can push to the branch' do - let(:branch_policy) { :developers_can_merge } + context 'when no one can create the tag' do + before do + create(:protected_tag, :no_one_can_create, + name: pipeline.ref, project: project) + + pipeline.update(tag: true) + end + + it 'does not include ability to update pipeline' do + expect(policy).to be_disallowed :update_pipeline + end + end + + context 'when no one can create the tag but it is not a tag' do + before do + create(:protected_tag, :no_one_can_create, + name: pipeline.ref, project: project) + end it 'includes ability to update pipeline' do expect(policy).to be_allowed :update_pipeline From 1ed6d1541c7973c08cdd4c1906ddcc0c3db893e3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 18 Jul 2017 22:13:57 +0800 Subject: [PATCH 026/143] Rename :user_cannot_update to :protected_ref --- app/policies/ci/build_policy.rb | 4 ++-- app/policies/ci/pipeline_policy.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 00f18d0155b..984e5482288 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,6 +1,6 @@ module Ci class BuildPolicy < CommitStatusPolicy - condition(:user_cannot_update) do + condition(:protected_ref) do access = ::Gitlab::UserAccess.new(@user, project: @subject.project) if @subject.tag? @@ -10,6 +10,6 @@ module Ci end end - rule { user_cannot_update }.prevent :update_build + rule { protected_ref }.prevent :update_build end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 07d724c9cfd..4e689a9efd5 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -2,7 +2,7 @@ module Ci class PipelinePolicy < BasePolicy delegate { @subject.project } - condition(:user_cannot_update) do + condition(:protected_ref) do access = ::Gitlab::UserAccess.new(@user, project: @subject.project) if @subject.tag? @@ -12,6 +12,6 @@ module Ci end end - rule { user_cannot_update }.prevent :update_pipeline + rule { protected_ref }.prevent :update_pipeline end end From 7bd5e571256aff6de132b118f04224e56abf3228 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 18 Jul 2017 22:32:34 +0800 Subject: [PATCH 027/143] Instead of adding master, stub_not_protect_default_branch --- spec/controllers/projects/jobs_controller_spec.rb | 14 +++++++++----- .../projects/pipelines_controller_spec.rb | 3 ++- spec/lib/gitlab/ci/status/build/cancelable_spec.rb | 4 +++- spec/lib/gitlab/ci/status/build/factory_spec.rb | 14 +++++++------- spec/lib/gitlab/ci/status/build/retryable_spec.rb | 4 +++- spec/lib/gitlab/ci/status/build/stop_spec.rb | 4 +++- spec/models/ci/pipeline_spec.rb | 8 ++++++-- spec/serializers/job_entity_spec.rb | 11 ++++++++--- spec/serializers/pipeline_details_entity_spec.rb | 6 ++++-- spec/serializers/pipeline_entity_spec.rb | 6 ++++-- spec/services/ci/process_pipeline_service_spec.rb | 4 +++- spec/services/ci/retry_build_service_spec.rb | 8 ++++++-- spec/services/create_deployment_service_spec.rb | 4 +++- spec/support/stub_configuration.rb | 5 +++++ 14 files changed, 66 insertions(+), 29 deletions(-) diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 9ed48d98360..5a295ae47a6 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -7,6 +7,10 @@ describe Projects::JobsController do let(:pipeline) { create(:ci_pipeline, project: project) } let(:user) { create(:user) } + before do + stub_not_protect_default_branch + end + describe 'GET index' do context 'when scope is pending' do before do @@ -218,7 +222,7 @@ describe Projects::JobsController do describe 'POST retry' do before do - project.add_master(user) + project.add_developer(user) sign_in(user) post_retry @@ -250,7 +254,7 @@ describe Projects::JobsController do describe 'POST play' do before do - project.add_master(user) + project.add_developer(user) create(:protected_branch, :developers_can_merge, name: 'master', project: project) @@ -290,7 +294,7 @@ describe Projects::JobsController do describe 'POST cancel' do before do - project.add_master(user) + project.add_developer(user) sign_in(user) post_cancel @@ -326,7 +330,7 @@ describe Projects::JobsController do describe 'POST cancel_all' do before do - project.add_master(user) + project.add_developer(user) sign_in(user) end @@ -368,7 +372,7 @@ describe Projects::JobsController do describe 'POST erase' do before do - project.add_master(user) + project.add_developer(user) sign_in(user) post_erase diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 3b4d7d069c9..c8de275ca3e 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -8,7 +8,8 @@ describe Projects::PipelinesController do let(:feature) { ProjectFeature::DISABLED } before do - project.add_master(user) + stub_not_protect_default_branch + project.add_developer(user) project.project_feature.update( builds_access_level: feature) diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index e7b880c9b09..5a7a42d84c0 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -48,7 +48,9 @@ describe Gitlab::Ci::Status::Build::Cancelable do describe '#has_action?' do context 'when user is allowed to update build' do before do - build.project.add_master(user) + stub_not_protect_default_branch + + build.project.add_developer(user) end it { is_expected.to have_action } diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index bc21b8af67c..8768302eda1 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -7,7 +7,9 @@ describe Gitlab::Ci::Status::Build::Factory do let(:factory) { described_class.new(build, user) } before do - project.add_master(user) + stub_not_protect_default_branch + + project.add_developer(user) end context 'when build is successful' do @@ -232,11 +234,10 @@ describe Gitlab::Ci::Status::Build::Factory do context 'when user does not have ability to play action' do before do - project.team.truncate - project.add_developer(user) + allow(build.project).to receive(:empty_repo?).and_return(false) create(:protected_branch, :no_one_can_push, - name: build.ref, project: project) + name: build.ref, project: build.project) end it 'fabricates status that has no action' do @@ -264,11 +265,10 @@ describe Gitlab::Ci::Status::Build::Factory do context 'when user is not allowed to execute manual action' do before do - project.team.truncate - project.add_developer(user) + allow(build.project).to receive(:empty_repo?).and_return(false) create(:protected_branch, :no_one_can_push, - name: build.ref, project: project) + name: build.ref, project: build.project) end it 'fabricates status with correct details' do diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index ed9752b4ed6..21026f2c968 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -48,7 +48,9 @@ describe Gitlab::Ci::Status::Build::Retryable do describe '#has_action?' do context 'when user is allowed to update build' do before do - build.project.add_master(user) + stub_not_protect_default_branch + + build.project.add_developer(user) end it { is_expected.to have_action } diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 7fe3cf7ea6d..e0425103f41 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -20,7 +20,9 @@ describe Gitlab::Ci::Status::Build::Stop do describe '#has_action?' do context 'when user is allowed to update build' do before do - build.project.add_master(user) + stub_not_protect_default_branch + + build.project.add_developer(user) end it { is_expected.to have_action } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index bdfe8706b5e..bbd45f10b1b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -734,8 +734,10 @@ describe Ci::Pipeline, models: true do context 'on failure and build retry' do before do + stub_not_protect_default_branch + build.drop - project.add_master(user) + project.add_developer(user) Ci::Build.retry(build, user) end @@ -999,7 +1001,9 @@ describe Ci::Pipeline, models: true do let(:latest_status) { pipeline.statuses.latest.pluck(:status) } before do - project.add_master(user) + stub_not_protect_default_branch + + project.add_developer(user) end context 'when there is a failed build and failed external status' do diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index ec30816654b..026360e91a3 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -7,8 +7,10 @@ describe JobEntity do let(:request) { double('request') } before do + stub_not_protect_default_branch allow(request).to receive(:current_user).and_return(user) - project.add_master(user) + + project.add_developer(user) end let(:entity) do @@ -77,7 +79,7 @@ describe JobEntity do project.add_developer(user) create(:protected_branch, :developers_can_merge, - name: 'master', project: project) + name: job.ref, project: job.project) end it 'contains path to play action' do @@ -91,7 +93,10 @@ describe JobEntity do context 'when user is not allowed to trigger action' do before do - project.team.truncate + allow(job.project).to receive(:empty_repo?).and_return(false) + + create(:protected_branch, :no_one_can_push, + name: job.ref, project: job.project) end it 'does not contain path to play action' do diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb index e9b24b47900..b990370a271 100644 --- a/spec/serializers/pipeline_details_entity_spec.rb +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -9,6 +9,8 @@ describe PipelineDetailsEntity do end before do + stub_not_protect_default_branch + allow(request).to receive(:current_user).and_return(user) end @@ -52,7 +54,7 @@ describe PipelineDetailsEntity do context 'user has ability to retry pipeline' do before do - project.add_master(user) + project.add_developer(user) end it 'retryable flag is true' do @@ -80,7 +82,7 @@ describe PipelineDetailsEntity do context 'user has ability to cancel pipeline' do before do - project.add_master(user) + project.add_developer(user) end it 'cancelable flag is true' do diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index 46433867b11..5b01cc4fc9e 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -5,6 +5,8 @@ describe PipelineEntity do let(:request) { double('request') } before do + stub_not_protect_default_branch + allow(request).to receive(:current_user).and_return(user) end @@ -52,7 +54,7 @@ describe PipelineEntity do context 'user has ability to retry pipeline' do before do - project.add_master(user) + project.add_developer(user) end it 'contains retry path' do @@ -80,7 +82,7 @@ describe PipelineEntity do context 'user has ability to cancel pipeline' do before do - project.add_master(user) + project.add_developer(user) end it 'contains cancel path' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 1e938a97f5a..5a34ec12c8f 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -9,7 +9,9 @@ describe Ci::ProcessPipelineService, '#execute', :services do end before do - project.add_master(user) + stub_not_protect_default_branch + + project.add_developer(user) end context 'when simple pipeline is defined' do diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 52c6a4a0bc8..2cf62b54666 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -85,7 +85,9 @@ describe Ci::RetryBuildService, :services do context 'when user has ability to execute build' do before do - project.add_master(user) + stub_not_protect_default_branch + + project.add_developer(user) end it_behaves_like 'build duplication' @@ -131,7 +133,9 @@ describe Ci::RetryBuildService, :services do context 'when user has ability to execute build' do before do - project.add_master(user) + stub_not_protect_default_branch + + project.add_developer(user) end it_behaves_like 'build duplication' diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 844d9d63428..2794721e157 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -244,7 +244,9 @@ describe CreateDeploymentService, services: true do context 'when job is retried' do it_behaves_like 'creates deployment' do before do - project.add_master(user) + stub_not_protect_default_branch + + project.add_developer(user) end let(:deployable) { Ci::Build.retry(job, user) } diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 48f454c7187..80ecce92dc1 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -9,6 +9,11 @@ module StubConfiguration .to receive_messages(messages) end + def stub_not_protect_default_branch + stub_application_setting( + default_branch_protection: Gitlab::Access::PROTECTION_NONE) + end + def stub_config_setting(messages) allow(Gitlab.config.gitlab).to receive_messages(messages) end From b84eb3434d0493cd594eade68d344a9675d72b8a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 19 Jul 2017 16:42:47 +0800 Subject: [PATCH 028/143] Try to merge permission checks into one --- app/services/ci/create_pipeline_service.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 8b689968895..f331f86e622 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -19,18 +19,20 @@ module Ci return error('Pipeline is disabled') end - unless trigger_request || can?(current_user, :create_pipeline, project) - return error('Insufficient permissions to create a new pipeline') + triggering_user = current_user || trigger_request.trigger.owner + + unless allowed_to_trigger_pipeline?(triggering_user) + if can?(triggering_user, :create_pipeline, project) + return error("Insufficient permissions for protected ref '#{ref}'") + else + return error('Insufficient permissions to create a new pipeline') + end end unless branch? || tag? return error('Reference not found') end - unless triggering_user_allowed_for_ref?(trigger_request) - return error("Insufficient permissions for protected ref '#{ref}'") - end - unless commit return error('Commit not found') end @@ -74,9 +76,7 @@ module Ci pipeline.tap(&:process!) end - def triggering_user_allowed_for_ref?(trigger_request) - triggering_user = current_user || trigger_request.trigger.owner - + def allowed_to_trigger_pipeline?(triggering_user) if triggering_user allowed_to_create?(triggering_user) else # legacy triggers don't have a corresponding user @@ -87,7 +87,7 @@ module Ci def allowed_to_create?(triggering_user) access = Gitlab::UserAccess.new(triggering_user, project: project) - Ability.allowed?(triggering_user, :create_pipeline, project) && + can?(triggering_user, :create_pipeline, project) && if branch? access.can_update_branch?(ref) elsif tag? From 561bc570dea970328e0c33972fcf1ed90427f2f2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 19 Jul 2017 17:53:56 +0800 Subject: [PATCH 029/143] Add a test for checking queries with different ref --- spec/serializers/pipeline_serializer_spec.rb | 33 ++++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 8dc666586c7..262bc4acb69 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -108,14 +108,35 @@ describe PipelineSerializer do end end - it 'verifies number of queries', :request_store do - recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.count).to be_within(1).of(59) - expect(recorded.cached_count).to eq(0) + shared_examples 'no N+1 queries' do + it 'verifies number of queries', :request_store do + recorded = ActiveRecord::QueryRecorder.new { subject } + expect(recorded.count).to be_within(1).of(59) + expect(recorded.cached_count).to eq(0) + end + end + + context 'with the same ref' do + let(:ref) { 'feature' } + + it_behaves_like 'no N+1 queries' + end + + context 'with different refs' do + def ref + @sequence ||= 0 + @sequence += 1 + "feature-#{@sequence}" + end + + it_behaves_like 'no N+1 queries' end def create_pipeline(status) - create(:ci_empty_pipeline, project: project, status: status).tap do |pipeline| + create(:ci_empty_pipeline, + project: project, + status: status, + ref: ref).tap do |pipeline| Ci::Build::AVAILABLE_STATUSES.each do |status| create_build(pipeline, status, status) end @@ -125,7 +146,7 @@ describe PipelineSerializer do def create_build(pipeline, stage, status) create(:ci_build, :tags, :triggered, :artifacts, pipeline: pipeline, stage: stage, - name: stage, status: status) + name: stage, status: status, ref: pipeline.ref) end end end From bab44bd99433a77fa45802647d767f0ca94a4a5e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 19 Jul 2017 13:11:39 +0200 Subject: [PATCH 030/143] Fix job merge request link to a forked source project --- app/serializers/build_details_entity.rb | 2 +- spec/serializers/build_details_entity_spec.rb | 83 +++++++++++++++---- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 20f9938f038..8ad5af1987c 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -16,7 +16,7 @@ class BuildDetailsEntity < JobEntity end expose :path do |build| - project_merge_request_path(project, build.merge_request) + project_merge_request_path(build.project, build.merge_request) end end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index b92c1c28ba8..e688035cecc 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -9,37 +9,86 @@ describe BuildDetailsEntity do describe '#as_json' do let(:project) { create(:project, :repository) } - let!(:build) { create(:ci_build, :failed, project: project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, :failed, pipeline: pipeline) } let(:request) { double('request') } - let(:entity) { described_class.new(build, request: request, current_user: user, project: project) } + + let(:entity) do + described_class.new(build, request: request, + current_user: user, + project: project) + end + subject { entity.as_json } before do allow(request).to receive(:current_user).and_return(user) end + it 'contains the needed key value pairs' do + expect(subject).to include(:coverage, :erased_at, :duration) + expect(subject).to include(:runner, :pipeline) + expect(subject).to include(:raw_path, :new_issue_path) + end + context 'when the user has access to issues and merge requests' do - let!(:merge_request) do - create(:merge_request, source_project: project, source_branch: build.ref) + context 'when merge request orginates from the same project' do + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: build.ref) + end + + before do + allow(build).to receive(:merge_request).and_return(merge_request) + end + + it 'contains the needed key value pairs' do + expect(subject).to include(:merge_request) + expect(subject).to include(:new_issue_path) + end + + it 'exposes details of the merge request' do + expect(subject[:merge_request]).to include(:iid, :path) + end + + it 'has a correct merge request path' do + expect(subject[:merge_request][:path]).to include project.full_path + end end - before do - allow(build).to receive(:merge_request).and_return(merge_request) - end + context 'when merge request is from a fork' do + let(:fork_project) do + create(:empty_project, forked_from_project: project) + end - it 'contains the needed key value pairs' do - expect(subject).to include(:coverage, :erased_at, :duration) - expect(subject).to include(:runner, :pipeline) - expect(subject).to include(:raw_path, :merge_request) - expect(subject).to include(:new_issue_path) - end + let(:pipeline) { create(:ci_pipeline, project: fork_project) } - it 'exposes details of the merge request' do - expect(subject[:merge_request]).to include(:iid, :path) + before do + allow(build).to receive(:merge_request).and_return(merge_request) + end + + let(:merge_request) do + create(:merge_request, source_project: fork_project, + target_project: project, + source_branch: build.ref) + end + + it 'contains the needed key value pairs' do + expect(subject).to include(:merge_request) + expect(subject).to include(:new_issue_path) + end + + it 'exposes details of the merge request' do + expect(subject[:merge_request]).to include(:iid, :path) + end + + it 'has a correct merge request path' do + expect(subject[:merge_request][:path]) + .to include fork_project.full_path + end end context 'when the build has been erased' do - let!(:build) { create(:ci_build, :erasable, project: project) } + let(:build) { create(:ci_build, :erasable, project: project) } it 'exposes the user whom erased the build' do expect(subject).to include(:erase_path) @@ -47,7 +96,7 @@ describe BuildDetailsEntity do end context 'when the build has been erased' do - let!(:build) { create(:ci_build, erased_at: Time.now, project: project, erased_by: user) } + let(:build) { create(:ci_build, erased_at: Time.now, project: project, erased_by: user) } it 'exposes the user whom erased the build' do expect(subject).to include(:erased_by) From a397a0eb1a4c34c27175e2c4e68e7ceb43a81f02 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 19 Jul 2017 19:12:11 +0800 Subject: [PATCH 031/143] Eliminate N+1 queries on checking different protected refs I realized where the N+1 queries were actually coming from project.protected_branches, but how come we cannot preload this, or cache this at all? Then I found that this is somehow a Rails limitation. What we're doing before, eventually come to: project.protected_branches.matching But why it's not cached? (project.protected_branches.loaded? is always false) It's because matching is a class method, which is called on the proxy. In this case, Rails cannot cache the result. I don't know if this is possible to implement or not, because clearly this would require some tricks to implement class methods on associations. So instead, we could just pass project.protected_branches to ProtectedRef.matching, then it would work regularly. With this change, there's no more N+1 queries. --- app/models/concerns/protected_ref.rb | 9 +++++---- lib/gitlab/user_access.rb | 30 +++++++++++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index fc6b840f7a8..ca9ef2b9375 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -25,8 +25,8 @@ module ProtectedRef end end - def protected_ref_accessible_to?(ref, user, action:) - access_levels_for_ref(ref, action: action).any? do |access_level| + def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil) + access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level| access_level.check_access(user) end end @@ -37,8 +37,9 @@ module ProtectedRef end end - def access_levels_for_ref(ref, action:) - self.matching(ref).map(&:"#{action}_access_levels").flatten + def access_levels_for_ref(ref, action:, protected_refs: nil) + self.matching(ref, protected_refs: protected_refs) + .map(&:"#{action}_access_levels").flatten end def matching(ref_name, protected_refs: nil) diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 25698bb8e99..6c6111006b6 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -37,8 +37,8 @@ module Gitlab request_cache def can_create_tag?(ref) return false unless can_access_git? - if ProtectedTag.protected?(project, ref) - project.protected_tags.protected_ref_accessible_to?(ref, user, action: :create) + if protected?(ProtectedTag, project, ref) + protected_tag_accessible_to?(ref, action: :create) else user.can?(:push_code, project) end @@ -47,7 +47,7 @@ module Gitlab request_cache def can_delete_branch?(ref) return false unless can_access_git? - if ProtectedBranch.protected?(project, ref) + if protected?(ProtectedBranch, project, ref) user.can?(:delete_protected_branch, project) else user.can?(:push_code, project) @@ -61,10 +61,10 @@ module Gitlab request_cache def can_push_to_branch?(ref) return false unless can_access_git? - if ProtectedBranch.protected?(project, ref) + if protected?(ProtectedBranch, project, ref) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) - project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push) + protected_branch_accessible_to?(ref, action: :push) else user.can?(:push_code, project) end @@ -73,8 +73,8 @@ module Gitlab request_cache def can_merge_to_branch?(ref) return false unless can_access_git? - if ProtectedBranch.protected?(project, ref) - project.protected_branches.protected_ref_accessible_to?(ref, user, action: :merge) + if protected?(ProtectedBranch, project, ref) + protected_branch_accessible_to?(ref, action: :merge) else user.can?(:push_code, project) end @@ -91,5 +91,21 @@ module Gitlab def can_access_git? user && user.can?(:access_git) end + + def protected_branch_accessible_to?(ref, action:) + ProtectedBranch.protected_ref_accessible_to?( + ref, user, action: action, + protected_refs: project.protected_branches) + end + + def protected_tag_accessible_to?(ref, action:) + ProtectedTag.protected_ref_accessible_to?( + ref, user, action: action, + protected_refs: project.protected_tags) + end + + request_cache def protected?(kind, project, ref) + kind.protected?(project, ref) + end end end From 0275914919551de1ffd5819bd9da7bf05d6a7668 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 19 Jul 2017 13:15:16 +0200 Subject: [PATCH 032/143] Add changelog entry for build merge request link fix --- .../fix-gb-fix-build-merge-request-link-to-fork-project.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-gb-fix-build-merge-request-link-to-fork-project.yml diff --git a/changelogs/unreleased/fix-gb-fix-build-merge-request-link-to-fork-project.yml b/changelogs/unreleased/fix-gb-fix-build-merge-request-link-to-fork-project.yml new file mode 100644 index 00000000000..7a68e91c6d3 --- /dev/null +++ b/changelogs/unreleased/fix-gb-fix-build-merge-request-link-to-fork-project.yml @@ -0,0 +1,4 @@ +--- +title: Fix job merge request link to a forked source project +merge_request: 12965 +author: From d035d735242a47bee7cd5973c9daa7d984800700 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 19 Jul 2017 22:37:38 +0800 Subject: [PATCH 033/143] Fix tests and fine tweak permission error message --- app/services/ci/create_pipeline_service.rb | 10 +++++----- lib/gitlab/user_access.rb | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index f331f86e622..700ac42d56e 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -23,16 +23,16 @@ module Ci unless allowed_to_trigger_pipeline?(triggering_user) if can?(triggering_user, :create_pipeline, project) - return error("Insufficient permissions for protected ref '#{ref}'") + if branch? || tag? + return error("Insufficient permissions for protected ref '#{ref}'") + else + return error('Reference not found') + end else return error('Insufficient permissions to create a new pipeline') end end - unless branch? || tag? - return error('Reference not found') - end - unless commit return error('Commit not found') end diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 6c6111006b6..d9a5af09f08 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -94,13 +94,15 @@ module Gitlab def protected_branch_accessible_to?(ref, action:) ProtectedBranch.protected_ref_accessible_to?( - ref, user, action: action, + ref, user, + action: action, protected_refs: project.protected_branches) end def protected_tag_accessible_to?(ref, action:) ProtectedTag.protected_ref_accessible_to?( - ref, user, action: action, + ref, user, + action: action, protected_refs: project.protected_tags) end From a05bc477b99500fa919295e1086f7a8de903e3c4 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 20 Jul 2017 00:08:34 +0800 Subject: [PATCH 034/143] Use hash to return multiple objects --- .../ci/create_trigger_request_service.rb | 8 +++--- lib/api/triggers.rb | 4 +-- lib/api/v3/triggers.rb | 6 ++--- lib/ci/api/triggers.rb | 6 ++--- .../ci/create_trigger_request_service_spec.rb | 26 +++++++++---------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 90f75606ddf..1674830a41a 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,13 +1,13 @@ module Ci - class CreateTriggerRequestService - def execute(project, trigger, ref, variables = nil) + module CreateTriggerRequestService + def self.execute(project, trigger, ref, variables = nil) trigger_request = trigger.trigger_requests.create(variables: variables) pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref) .execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) - trigger_request.pipeline = pipeline - trigger_request + { trigger_request: trigger_request, + pipeline: pipeline } end end end diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 9e444563fdf..55528101f15 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -27,8 +27,8 @@ module API end # create request and trigger builds - trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) - pipeline = trigger_request.pipeline + result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables) + pipeline = result[:pipeline] if pipeline.persisted? present pipeline, with: Entities::Pipeline diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb index 7e75c579528..0e236423b8c 100644 --- a/lib/api/v3/triggers.rb +++ b/lib/api/v3/triggers.rb @@ -28,11 +28,11 @@ module API end # create request and trigger builds - trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) - pipeline = trigger_request.pipeline + result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables) + pipeline = result[:pipeline] if pipeline.persisted? - present trigger_request, with: ::API::V3::Entities::TriggerRequest + present result[:trigger_request], with: ::API::V3::Entities::TriggerRequest else render_validation_error!(pipeline) end diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb index 0e5174e13ab..ce0ef95b186 100644 --- a/lib/ci/api/triggers.rb +++ b/lib/ci/api/triggers.rb @@ -24,11 +24,11 @@ module Ci end # create request and trigger builds - trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref], variables) - pipeline = trigger_request.pipeline + result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref], variables) + pipeline = result[:pipeline] if pipeline.persisted? - present trigger_request, with: Entities::TriggerRequest + present result[:trigger_request], with: Entities::TriggerRequest else render_validation_error!(pipeline) end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index 8582c74e734..48d9b0844f1 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService, services: true do - let(:service) { described_class.new } + let(:service) { described_class } let(:project) { create(:project, :repository) } let(:trigger) { create(:ci_trigger, project: project, owner: owner) } let(:owner) { create(:user) } @@ -17,26 +17,26 @@ describe Ci::CreateTriggerRequestService, services: true do subject { service.execute(project, trigger, 'master') } context 'without owner' do - 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 } - it { expect(subject.builds.first).to be_kind_of(Ci::Build) } + it { expect(subject[:trigger_request]).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject[:trigger_request].builds.first).to be_kind_of(Ci::Build) } + it { expect(subject[:pipeline]).to be_kind_of(Ci::Pipeline) } + it { expect(subject[:pipeline]).to be_trigger } end context 'with owner' do - 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 } - it { expect(subject.pipeline.user).to eq(owner) } - it { expect(subject.builds.first).to be_kind_of(Ci::Build) } - it { expect(subject.builds.first.user).to eq(owner) } + it { expect(subject[:trigger_request]).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject[:trigger_request].builds.first).to be_kind_of(Ci::Build) } + it { expect(subject[:trigger_request].builds.first.user).to eq(owner) } + it { expect(subject[:pipeline]).to be_kind_of(Ci::Pipeline) } + it { expect(subject[:pipeline]).to be_trigger } + it { expect(subject[:pipeline].user).to eq(owner) } end end context 'no commit for ref' do subject { service.execute(project, trigger, 'other-branch') } - it { expect(subject.pipeline).not_to be_persisted } + 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.pipeline).not_to be_persisted } + it { expect(subject[:pipeline]).not_to be_persisted } end end end From c9c715cd5510456d83da5272f28b7ce7f248c77f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 20 Jul 2017 01:31:20 +0800 Subject: [PATCH 035/143] Make permission checks easier to understand --- app/services/ci/create_pipeline_service.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 700ac42d56e..5da70ba87e9 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -23,16 +23,16 @@ module Ci unless allowed_to_trigger_pipeline?(triggering_user) if can?(triggering_user, :create_pipeline, project) - if branch? || tag? - return error("Insufficient permissions for protected ref '#{ref}'") - else - return error('Reference not found') - end + return error("Insufficient permissions for protected ref '#{ref}'") else return error('Insufficient permissions to create a new pipeline') end end + unless branch? || tag? + return error('Reference not found') + end + unless commit return error('Commit not found') end @@ -93,7 +93,7 @@ module Ci elsif tag? access.can_create_tag?(ref) else - false + true # Allow it for now and we'll reject when we check ref existence end end From e9a25242a16c0b8092fcc94dfb117ac214be8205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Thu, 13 Jul 2017 09:54:28 +0800 Subject: [PATCH 036/143] Add uk translation difference of Pipeline Schedules --- locale/uk/part.po | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 locale/uk/part.po diff --git a/locale/uk/part.po b/locale/uk/part.po new file mode 100644 index 00000000000..ef5864be5c9 --- /dev/null +++ b/locale/uk/part.po @@ -0,0 +1,38 @@ +# Андрей Витюк , 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-07-12 07:29-0400\n" +"Last-Translator: Андрей Витюк \n" +"Language-Team: Ukrainian\n" +"Language: uk\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgid "PipelineSchedules|Input variable key" +msgstr "Введіть ім'я змінної" + +msgid "PipelineSchedules|Input variable value" +msgstr "Вхідні значення змінних" + +msgid "PipelineSchedules|Remove variable row" +msgstr "Видалити змінні" + +msgid "PipelineSchedules|Variables" +msgstr "Змінні" + +msgid "" +"You are going to remove %{group_name}.\n" +"Removed groups CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" +"Ви хочете видалити %{group_name}.\n" +"Видалені групи НЕ МОЖНА буду відновити!\n" +"Ви АБСОЛЮТНО впевнені?" + From 77c14bee90cb61c09fcae0515688c5ebb8781892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Thu, 20 Jul 2017 10:08:51 +0800 Subject: [PATCH 037/143] merge uk part.po to gitlab.po --- locale/uk/gitlab.po | 29 +++++++++++++++++++++++++---- locale/uk/part.po | 38 -------------------------------------- 2 files changed, 25 insertions(+), 42 deletions(-) delete mode 100644 locale/uk/part.po diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po index 59a7eb6e1b3..56498f3c901 100644 --- a/locale/uk/gitlab.po +++ b/locale/uk/gitlab.po @@ -1,16 +1,16 @@ -# Андрей Витюк , 2017. #zanata # Huang Tao , 2017. #zanata +# Андрей Витюк , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-06-28 13:32+0200\n" +"POT-Creation-Date: 2017-07-05 08:50-0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2017-07-12 09:05-0400\n" -"Last-Translator: Андрей Витюк \n" "Language-Team: Ukrainian (https://translate.zanata.org/project/view/GitLab)\n" +"PO-Revision-Date: 2017-07-14 01:22-0400\n" +"Last-Translator: Huang Tao \n" "Language: uk\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " @@ -654,6 +654,12 @@ msgstr "Всі" msgid "PipelineSchedules|Inactive" msgstr "Неактивні" +msgid "PipelineSchedules|Input variable key" +msgstr "Введіть ім'я змінної" + +msgid "PipelineSchedules|Input variable value" +msgstr "Вхідні значення змінних" + msgid "PipelineSchedules|Next Run" msgstr "Наступний запуск" @@ -663,12 +669,18 @@ msgstr "Немає" msgid "PipelineSchedules|Provide a short description for this pipeline" msgstr "Задайте короткий опис для цього Конвеєру" +msgid "PipelineSchedules|Remove variable row" +msgstr "Видалити змінні" + msgid "PipelineSchedules|Take ownership" msgstr "Стати власником" msgid "PipelineSchedules|Target" msgstr "Ціль" +msgid "PipelineSchedules|Variables" +msgstr "Змінні" + msgid "PipelineSheduleIntervalPattern|Custom" msgstr "Власні" @@ -1140,6 +1152,15 @@ msgstr "Ми не маємо достатньо даних для показу msgid "Withdraw Access Request" msgstr "Скасувати запит доступу" +msgid "" +"You are going to remove %{group_name}.\n" +"Removed groups CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" +"Ви хочете видалити %{group_name}.\n" +"Видалені групи НЕ МОЖНА буду відновити!\n" +"Ви АБСОЛЮТНО впевнені?" + msgid "" "You are going to remove %{project_name_with_namespace}.\n" "Removed project CANNOT be restored!\n" diff --git a/locale/uk/part.po b/locale/uk/part.po deleted file mode 100644 index ef5864be5c9..00000000000 --- a/locale/uk/part.po +++ /dev/null @@ -1,38 +0,0 @@ -# Андрей Витюк , 2017. #zanata -msgid "" -msgstr "" -"Project-Id-Version: gitlab 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-06-15 21:59-0500\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2017-07-12 07:29-0400\n" -"Last-Translator: Андрей Витюк \n" -"Language-Team: Ukrainian\n" -"Language: uk\n" -"X-Generator: Zanata 3.9.6\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -msgid "PipelineSchedules|Input variable key" -msgstr "Введіть ім'я змінної" - -msgid "PipelineSchedules|Input variable value" -msgstr "Вхідні значення змінних" - -msgid "PipelineSchedules|Remove variable row" -msgstr "Видалити змінні" - -msgid "PipelineSchedules|Variables" -msgstr "Змінні" - -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Ви хочете видалити %{group_name}.\n" -"Видалені групи НЕ МОЖНА буду відновити!\n" -"Ви АБСОЛЮТНО впевнені?" - From c9749e22383661c0772addfcf4274ec3a81bd229 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 20 Jul 2017 09:18:45 +0200 Subject: [PATCH 038/143] Improve build details serializable entity specs --- spec/factories/ci/builds.rb | 1 + spec/serializers/build_details_entity_spec.rb | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index a77f01ecb00..863c82ece6a 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -140,6 +140,7 @@ FactoryGirl.define do end trait :erased do + erasable erased_at Time.now erased_by factory: :user end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index e688035cecc..2c981154f0d 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -46,8 +46,8 @@ describe BuildDetailsEntity do expect(subject).to include(:new_issue_path) end - it 'exposes details of the merge request' do - expect(subject[:merge_request]).to include(:iid, :path) + it 'exposes correct details of the merge request' do + expect(subject[:merge_request][:iid]).to eq merge_request.iid end it 'has a correct merge request path' do @@ -78,7 +78,7 @@ describe BuildDetailsEntity do end it 'exposes details of the merge request' do - expect(subject[:merge_request]).to include(:iid, :path) + expect(subject[:merge_request][:iid]).to eq merge_request.iid end it 'has a correct merge request path' do @@ -88,7 +88,7 @@ describe BuildDetailsEntity do end context 'when the build has been erased' do - let(:build) { create(:ci_build, :erasable, project: project) } + let(:build) { create(:ci_build, :erased, project: project) } it 'exposes the user whom erased the build' do expect(subject).to include(:erase_path) @@ -96,7 +96,7 @@ describe BuildDetailsEntity do end context 'when the build has been erased' do - let(:build) { create(:ci_build, erased_at: Time.now, project: project, erased_by: user) } + let(:build) { create(:ci_build, :erased, project: project, erased_by: user) } it 'exposes the user whom erased the build' do expect(subject).to include(:erased_by) From 72a85ae9ac2468b099a565d3848bf8e0dcdf4499 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 28 Apr 2017 06:46:15 +0000 Subject: [PATCH 039/143] Handle errors while a project is being deleted asynchronously. 1. Rescue all errors that `Projects::DestroyService` might throw, to prevent the worker from leaving things in an inconsistent state 2. Unmark the project as `pending_delete` 3. Add a `delete_error` text column to `projects`, and save the error message in there, to be shown to the project masters/owners. --- app/services/projects/destroy_service.rb | 9 +-- app/workers/project_destroy_worker.rb | 3 + ...307_add_column_delete_error_to_projects.rb | 31 +++++++++ db/schema.rb | 1 + .../import_export/safe_model_attributes.yml | 1 + .../services/projects/destroy_service_spec.rb | 63 +++++++++++++++++++ 6 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20170428064307_add_column_delete_error_to_projects.rb diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index e2b2660ea71..682407ac896 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -26,9 +26,6 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute Project.transaction do - project.team.truncate - project.destroy! - unless remove_legacy_registry_tags raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') end @@ -40,10 +37,14 @@ module Projects unless remove_repository(wiki_path) raise_error('Failed to remove wiki repository. Please try again or contact administrator.') end + + project.team.truncate + project.destroy! end - log_info("Project \"#{project.path_with_namespace}\" was removed") system_hook_service.execute_hooks_for(project, :destroy) + + log_info("Project \"#{project.path_with_namespace}\" was removed") true end diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index b462327490e..482e1e38cd1 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -12,5 +12,8 @@ class ProjectDestroyWorker user = User.find(user_id) ::Projects::DestroyService.new(project, user, params.symbolize_keys).execute + rescue StandardError => error + project.assign_attributes(delete_error: error.message, pending_delete: false) + project.save!(validate: false) end end diff --git a/db/migrate/20170428064307_add_column_delete_error_to_projects.rb b/db/migrate/20170428064307_add_column_delete_error_to_projects.rb new file mode 100644 index 00000000000..ef5fc2cdea5 --- /dev/null +++ b/db/migrate/20170428064307_add_column_delete_error_to_projects.rb @@ -0,0 +1,31 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddColumnDeleteErrorToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :projects, :delete_error, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 284b2068166..0ba2bd31517 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1134,6 +1134,7 @@ ActiveRecord::Schema.define(version: 20170717150329) do t.integer "cached_markdown_version" t.datetime "last_repository_updated_at" t.string "ci_config_path" + t.text "delete_error" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 4ef3db3721f..0f2db3380a7 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -396,6 +396,7 @@ Project: - build_allow_git_fetch - last_repository_updated_at - ci_config_path +- delete_error Author: - name ProjectFeature: diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index b399d3402fd..a629afe723d 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -36,6 +36,27 @@ describe Projects::DestroyService, services: true do end end + shared_examples 'handles errors thrown during async destroy' do |error_message| + it 'does not allow the error to bubble up' do + expect do + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end.not_to raise_error + end + + it 'unmarks the project as "pending deletion"' do + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + + expect(project.reload.pending_delete).to be(false) + end + + it 'stores an error message in `projects.delete_error`' do + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + + expect(project.reload.delete_error).to be_present + expect(project.delete_error).to include(error_message) + end + end + context 'Sidekiq inline' do before do # Run sidekiq immediatly to check that renamed repository will be removed @@ -89,10 +110,52 @@ describe Projects::DestroyService, services: true do end it_behaves_like 'deleting the project with pipeline and build' + + context 'errors' do + context 'when `remove_legacy_registry_tags` fails' do + before do + expect_any_instance_of(Projects::DestroyService) + .to receive(:remove_legacy_registry_tags).and_return(false) + end + + it_behaves_like 'handles errors thrown during async destroy', "Failed to remove some tags" + end + + context 'when `remove_repository` fails' do + before do + expect_any_instance_of(Projects::DestroyService) + .to receive(:remove_repository).and_return(false) + end + + it_behaves_like 'handles errors thrown during async destroy', "Failed to remove project repository" + end + + context 'when `execute` raises any other error' do + before do + expect_any_instance_of(Projects::DestroyService) + .to receive(:execute).and_raise(ArgumentError.new("Other error message")) + end + + it_behaves_like 'handles errors thrown during async destroy', "Other error message" + end + end end context 'with execute' do it_behaves_like 'deleting the project with pipeline and build' + + context 'when `execute` raises an error' do + before do + expect_any_instance_of(Projects::DestroyService) + .to receive(:execute).and_raise(ArgumentError) + end + + it 'allows the error to bubble up' do + expect do + Sidekiq::Testing.inline! { Projects::DestroyService.new(project, user, {}).execute } + end.to raise_error(ArgumentError) + end + end end describe 'container registry' do From f0e4e3993b1f5a21ab61aaff95f73ac4e5b88ad3 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 4 May 2017 10:50:05 +0000 Subject: [PATCH 040/143] WIP: Display a project's `delete_error` on the project homepage. --- app/views/projects/_deletion_failed.html.haml | 9 +++++++++ app/views/projects/show.html.haml | 1 + 2 files changed, 10 insertions(+) create mode 100644 app/views/projects/_deletion_failed.html.haml diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml new file mode 100644 index 00000000000..cd717760432 --- /dev/null +++ b/app/views/projects/_deletion_failed.html.haml @@ -0,0 +1,9 @@ +- if @project.delete_error.present? + .project-deletion-failed-message.alert.alert-warning + This project was scheduled for deletion, but failed with the message: + = @project.delete_error + + .alert-link-group + = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' + | + = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 49d0a6828fe..336bc694ffc 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,6 +7,7 @@ = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") = content_for flash_message_container do + = render 'deletion_failed' - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' = render 'shared/no_password' From 3491b19a4e67a9f439c12afac45ef38f3fce0ef5 Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Thu, 29 Jun 2017 12:43:01 +0100 Subject: [PATCH 041/143] Add specs for ProjectDestroyWorker --- app/services/projects/destroy_service.rb | 2 +- app/views/projects/_deletion_failed.html.haml | 4 +-- app/views/projects/_flash_messages.html.haml | 5 ++++ app/views/projects/empty.html.haml | 5 +--- app/views/projects/show.html.haml | 6 +--- app/workers/project_destroy_worker.rb | 15 ++++------ ...307_add_column_delete_error_to_projects.rb | 24 --------------- spec/features/projects/show_project_spec.rb | 30 +++++++++++++++++++ spec/workers/project_destroy_worker_spec.rb | 21 ++++++++++--- 9 files changed, 63 insertions(+), 49 deletions(-) create mode 100644 app/views/projects/_flash_messages.html.haml create mode 100644 spec/features/projects/show_project_spec.rb diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 682407ac896..7b0a08af290 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -44,7 +44,7 @@ module Projects system_hook_service.execute_hooks_for(project, :destroy) - log_info("Project \"#{project.path_with_namespace}\" was removed") + log_info("Project \"#{project.full_path}\" was removed") true end diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml index cd717760432..028510b5671 100644 --- a/app/views/projects/_deletion_failed.html.haml +++ b/app/views/projects/_deletion_failed.html.haml @@ -1,9 +1,9 @@ - if @project.delete_error.present? .project-deletion-failed-message.alert.alert-warning - This project was scheduled for deletion, but failed with the message: + This project was scheduled for deletion, but failed with the following message: = @project.delete_error .alert-link-group - = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' + = link_to "Don't show again", profile_path(user: { hide_no_ssh_key: true }), method: :put, class: 'alert-link' | = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml new file mode 100644 index 00000000000..6c9d466c761 --- /dev/null +++ b/app/views/projects/_flash_messages.html.haml @@ -0,0 +1,5 @@ += content_for flash_message_container do + = render 'deletion_failed' + - if current_user && can?(current_user, :download_code, project) + = render 'shared/no_ssh' + = render 'shared/no_password' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 0f132a68ce1..3d7c72ae61a 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,10 +1,7 @@ - @no_container = true - flash_message_container = show_new_nav? ? :new_global_flash : :flash_message -= content_for flash_message_container do - - if current_user && can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - = render 'shared/no_password' += render partial: 'flash_messages', locals: { project: @project } = render "projects/head" = render "home_panel" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 336bc694ffc..3926149e790 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -6,11 +6,7 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") -= content_for flash_message_container do - = render 'deletion_failed' - - if current_user && can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - = render 'shared/no_password' += render partial: 'flash_messages', locals: { project: @project } = render "projects/head" = render "projects/last_push" diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index 482e1e38cd1..209cf11e893 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -3,17 +3,14 @@ class ProjectDestroyWorker include DedicatedSidekiqQueue def perform(project_id, user_id, params) - begin - project = Project.unscoped.find(project_id) - rescue ActiveRecord::RecordNotFound - return - end - + project = Project.find(project_id) user = User.find(user_id) ::Projects::DestroyService.new(project, user, params.symbolize_keys).execute - rescue StandardError => error - project.assign_attributes(delete_error: error.message, pending_delete: false) - project.save!(validate: false) + rescue Exception => error # rubocop:disable Lint/RescueException + project&.update_attributes(delete_error: error.message, pending_delete: false) + Rails.logger.error("Deletion failed on #{project&.full_path} with the following message: #{error.message}") + + raise end end diff --git a/db/migrate/20170428064307_add_column_delete_error_to_projects.rb b/db/migrate/20170428064307_add_column_delete_error_to_projects.rb index ef5fc2cdea5..09f9d9b5b7a 100644 --- a/db/migrate/20170428064307_add_column_delete_error_to_projects.rb +++ b/db/migrate/20170428064307_add_column_delete_error_to_projects.rb @@ -1,30 +1,6 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - class AddColumnDeleteErrorToProjects < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - # Set this constant to true if this migration requires downtime. DOWNTIME = false - # When a migration requires downtime you **must** uncomment the following - # constant and define a short and easy to understand explanation as to why the - # migration requires downtime. - # DOWNTIME_REASON = '' - - # When using the methods "add_concurrent_index", "remove_concurrent_index" or - # "add_column_with_default" you must disable the use of transactions - # as these methods can not run in an existing transaction. - # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure - # that either of them is the _only_ method called in the migration, - # any other changes should go in a separate migration. - # This ensures that upon failure _only_ the index creation or removing fails - # and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - def change add_column :projects, :delete_error, :text end diff --git a/spec/features/projects/show_project_spec.rb b/spec/features/projects/show_project_spec.rb new file mode 100644 index 00000000000..5aa0d8f0026 --- /dev/null +++ b/spec/features/projects/show_project_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe 'Project show page', feature: true do + context 'when project pending delete' do + let(:project) { create(:project, :empty_repo, pending_delete: true) } + let(:worker) { ProjectDestroyWorker.new } + + before do + sign_in(project.owner) + end + + it 'shows flash error if deletion for project fails' do + error_message = "some error message" + project.update_attributes(delete_error: error_message, pending_delete: false) + + visit namespace_project_path(project.namespace, project) + + expect(page).to have_selector('.project-deletion-failed-message') + expect(page).to have_content("This project was scheduled for deletion, but failed with the following message: #{error_message}") + end + + it 'renders 404 if project was successfully deleted' do + worker.perform(project.id, project.owner.id, {}) + + visit namespace_project_path(project.namespace, project) + + expect(page).to have_http_status(404) + end + end +end diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 3d135f40c1f..29f0295de42 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -1,24 +1,37 @@ require 'spec_helper' describe ProjectDestroyWorker do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, pending_delete: true) } let(:path) { project.repository.path_to_repo } subject { described_class.new } - describe "#perform" do - it "deletes the project" do + describe '#perform' do + it 'deletes the project' do subject.perform(project.id, project.owner.id, {}) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_falsey end - it "deletes the project but skips repo deletion" do + it 'deletes the project but skips repo deletion' do subject.perform(project.id, project.owner.id, { "skip_repo" => true }) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_truthy end + + describe 'when StandardError is raised' do + it 'reverts pending_delete attribute with a error message' do + allow_any_instance_of(::Projects::DestroyService).to receive(:execute).and_raise(StandardError, "some error message") + + expect do + subject.perform(project.id, project.owner.id, {}) + end.to change { project.reload.pending_delete }.from(true).to(false) + + expect(Project.all).to include(project) + expect(project.delete_error).to eq("some error message") + end + end end end From 0aa8249e484ca97cfc28c7301d69077919032c08 Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Thu, 6 Jul 2017 14:43:07 +0100 Subject: [PATCH 042/143] Refactors Project Destroy service and worker code --- app/services/projects/destroy_service.rb | 42 +++++++++++++++--------- app/workers/project_destroy_worker.rb | 7 ++-- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 7b0a08af290..7e38aacc91a 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -25,27 +25,15 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute - Project.transaction do - unless remove_legacy_registry_tags - raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') - end - - unless remove_repository(repo_path) - raise_error('Failed to remove project repository. Please try again or contact administrator.') - end - - unless remove_repository(wiki_path) - raise_error('Failed to remove wiki repository. Please try again or contact administrator.') - end - - project.team.truncate - project.destroy! - end + attempt_destroy_transaction(project, repo_path, wiki_path) system_hook_service.execute_hooks_for(project, :destroy) log_info("Project \"#{project.full_path}\" was removed") true + rescue Projects::DestroyService::DestroyError => error + Rails.logger.error("Deletion failed on #{project.full_path} with the following message: #{error.message}") + false end private @@ -71,6 +59,28 @@ module Projects end end + def attempt_destroy_transaction(project, repo_path, wiki_path) + Project.transaction do + unless remove_legacy_registry_tags + raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') + end + + unless remove_repository(repo_path) + raise_error('Failed to remove project repository. Please try again or contact administrator.') + end + + unless remove_repository(wiki_path) + raise_error('Failed to remove wiki repository. Please try again or contact administrator.') + end + + project.team.truncate + project.destroy! + end + rescue Exception => error # rubocop:disable Lint/RescueException + project.update_attributes(delete_error: error.message, pending_delete: false) + raise + end + ## # This method makes sure that we correctly remove registry tags # for legacy image repository (when repository path equals project path). diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index 209cf11e893..e695ec060f0 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -7,10 +7,7 @@ class ProjectDestroyWorker user = User.find(user_id) ::Projects::DestroyService.new(project, user, params.symbolize_keys).execute - rescue Exception => error # rubocop:disable Lint/RescueException - project&.update_attributes(delete_error: error.message, pending_delete: false) - Rails.logger.error("Deletion failed on #{project&.full_path} with the following message: #{error.message}") - - raise + rescue ActiveRecord::RecordNotFound => error + logger.error("Failed to delete project #{project.path_with_namespace} (#{project.id}): #{error.message}") end end From 70489d08b7e8b4bd0ba566da2ed0e417bef3ed3e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 20 Jul 2017 11:42:13 +0200 Subject: [PATCH 043/143] Fix invalid assertions in build details entity specs --- spec/factories/ci/builds.rb | 1 - spec/serializers/build_details_entity_spec.rb | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 863c82ece6a..a77f01ecb00 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -140,7 +140,6 @@ FactoryGirl.define do end trait :erased do - erasable erased_at Time.now erased_by factory: :user end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 2c981154f0d..446a2451956 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -87,18 +87,18 @@ describe BuildDetailsEntity do end end - context 'when the build has been erased' do - let(:build) { create(:ci_build, :erased, project: project) } + context 'when the build has not been erased' do + let(:build) { create(:ci_build, :erasable, project: project) } - it 'exposes the user whom erased the build' do + it 'exposes a build erase path' do expect(subject).to include(:erase_path) end end context 'when the build has been erased' do - let(:build) { create(:ci_build, :erased, project: project, erased_by: user) } + let(:build) { create(:ci_build, :erased, project: project) } - it 'exposes the user whom erased the build' do + it 'exposes the user who erased the build' do expect(subject).to include(:erased_by) end end From e9862a9900c6269a41b65ca543035e57b49fede3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 20 Jul 2017 20:17:42 +0800 Subject: [PATCH 044/143] Use struct instead of hash --- .../ci/create_trigger_request_service.rb | 5 ++-- lib/api/triggers.rb | 2 +- lib/api/v3/triggers.rb | 4 ++-- lib/ci/api/triggers.rb | 4 ++-- .../ci/create_trigger_request_service_spec.rb | 24 +++++++++---------- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 1674830a41a..a43d0e4593c 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,13 +1,14 @@ module Ci module CreateTriggerRequestService + Result = Struct.new(:trigger_request, :pipeline) + def self.execute(project, trigger, ref, variables = nil) trigger_request = trigger.trigger_requests.create(variables: variables) pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref) .execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request) - { trigger_request: trigger_request, - pipeline: pipeline } + Result.new(trigger_request, pipeline) end end end diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 55528101f15..280fe72ae47 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -28,7 +28,7 @@ module API # create request and trigger builds result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables) - pipeline = result[:pipeline] + pipeline = result.pipeline if pipeline.persisted? present pipeline, with: Entities::Pipeline diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb index 0e236423b8c..e9d4c35307b 100644 --- a/lib/api/v3/triggers.rb +++ b/lib/api/v3/triggers.rb @@ -29,10 +29,10 @@ module API # create request and trigger builds result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables) - pipeline = result[:pipeline] + pipeline = result.pipeline if pipeline.persisted? - present result[:trigger_request], with: ::API::V3::Entities::TriggerRequest + present result.trigger_request, with: ::API::V3::Entities::TriggerRequest else render_validation_error!(pipeline) end diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb index ce0ef95b186..6225203f223 100644 --- a/lib/ci/api/triggers.rb +++ b/lib/ci/api/triggers.rb @@ -25,10 +25,10 @@ module Ci # create request and trigger builds result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref], variables) - pipeline = result[:pipeline] + pipeline = result.pipeline if pipeline.persisted? - present result[:trigger_request], with: Entities::TriggerRequest + present result.trigger_request, with: Entities::TriggerRequest else render_validation_error!(pipeline) end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index 48d9b0844f1..37ca9804f56 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -17,26 +17,26 @@ describe Ci::CreateTriggerRequestService, services: true do subject { service.execute(project, trigger, 'master') } context 'without owner' do - it { expect(subject[:trigger_request]).to be_kind_of(Ci::TriggerRequest) } - it { expect(subject[:trigger_request].builds.first).to be_kind_of(Ci::Build) } - it { expect(subject[:pipeline]).to be_kind_of(Ci::Pipeline) } - it { expect(subject[:pipeline]).to be_trigger } + it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) } + it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(subject.pipeline).to be_trigger } end context 'with owner' do - it { expect(subject[:trigger_request]).to be_kind_of(Ci::TriggerRequest) } - it { expect(subject[:trigger_request].builds.first).to be_kind_of(Ci::Build) } - it { expect(subject[:trigger_request].builds.first.user).to eq(owner) } - it { expect(subject[:pipeline]).to be_kind_of(Ci::Pipeline) } - it { expect(subject[:pipeline]).to be_trigger } - it { expect(subject[:pipeline].user).to eq(owner) } + it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) } + it { expect(subject.trigger_request.builds.first.user).to eq(owner) } + it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(subject.pipeline).to be_trigger } + it { expect(subject.pipeline.user).to eq(owner) } end end context 'no commit for ref' do subject { service.execute(project, trigger, 'other-branch') } - it { expect(subject[:pipeline]).not_to be_persisted } + 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[:pipeline]).not_to be_persisted } + it { expect(subject.pipeline).not_to be_persisted } end end end From 01c9488f4a559063eba77074ba2d369de87b8018 Mon Sep 17 00:00:00 2001 From: Ryan Scott Date: Thu, 30 Mar 2017 10:39:06 +0900 Subject: [PATCH 045/143] Added slash command to close an issue as a duplicate. Closes #26372 --- app/models/system_note_metadata.rb | 2 +- app/services/issuable_base_service.rb | 22 ++++++++ .../quick_actions/interpret_service.rb | 14 +++++ app/services/system_note_service.rb | 19 +++++++ .../26372-duplicate-issue-slash-command.yml | 4 ++ doc/user/project/quick_actions.md | 1 + .../issues/user_uses_slash_commands_spec.rb | 41 ++++++++++++++ spec/services/issues/update_service_spec.rb | 56 +++++++++++++++++++ .../quick_actions/interpret_service_spec.rb | 36 ++++++++++++ spec/services/system_note_service_spec.rb | 25 +++++++++ 10 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/26372-duplicate-issue-slash-command.yml diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 414c95f7705..1ffdd285b91 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -2,7 +2,7 @@ class SystemNoteMetadata < ActiveRecord::Base ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved opened closed merged - outdated + outdated duplicate ].freeze validates :note, presence: true diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 9078b1f0983..c7e646222bb 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -46,6 +46,14 @@ class IssuableBaseService < BaseService SystemNoteService.change_time_spent(issuable, issuable.project, current_user) end + def create_issue_duplicate_note(issuable, original_issue) + SystemNoteService.mark_duplicate_issue(issuable, issuable.project, current_user, original_issue) + end + + def create_cross_reference_note(noteable, mentioner) + SystemNoteService.cross_reference(noteable, mentioner, current_user) + end + def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" @@ -58,6 +66,7 @@ class IssuableBaseService < BaseService params.delete(:assignee_ids) params.delete(:assignee_id) params.delete(:due_date) + params.delete(:original_issue_id) end filter_assignee(issuable) @@ -209,6 +218,7 @@ class IssuableBaseService < BaseService change_state(issuable) change_subscription(issuable) change_todo(issuable) + change_issue_duplicate(issuable) toggle_award(issuable) filter_params(issuable) old_labels = issuable.labels.to_a @@ -291,6 +301,18 @@ class IssuableBaseService < BaseService end end + def change_issue_duplicate(issuable) + original_issue_id = params.delete(:original_issue_id) + return if original_issue_id.nil? + + original_issue = IssuesFinder.new(current_user).find(original_issue_id) + if original_issue.present? + create_issue_duplicate_note(issuable, original_issue) + close_service.new(project, current_user, {}).execute(issuable) + create_cross_reference_note(original_issue, issuable) + end + end + def toggle_award(issuable) award = params.delete(:emoji_award) if award diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 6f82159e6c7..3eecf0b5545 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -471,6 +471,20 @@ module QuickActions end end + desc 'Mark this issue as a duplicate of another issue' + params '#issue' + condition do + issuable.is_a?(Issue) && + issuable.persisted? && + current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + end + command :duplicate do |duplicate_param| + original_issue = extract_references(duplicate_param, :issue).first + if original_issue.present? && original_issue != issuable + @updates[:original_issue_id] = original_issue.id + end + end + def extract_users(params) return [] if params.nil? diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index da0f21d449a..2e5e904c43d 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -552,6 +552,25 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'moved')) end + # Called when a Notable has been marked as a duplicate of another Issue + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # original_issue - Issue that this is a duplicate of + # + # Example Note text: + # + # "marked this issue as a duplicate of #1234" + # + # "marked this issue as a duplicate of other_project#5678" + # + # Returns the created Note object + def mark_duplicate_issue(noteable, project, author, original_issue) + body = "marked this issue as a duplicate of #{original_issue.to_reference(project)}" + create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) + end + private def notes_for_mentioner(mentioner, noteable, notes) diff --git a/changelogs/unreleased/26372-duplicate-issue-slash-command.yml b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml new file mode 100644 index 00000000000..079ebe59f98 --- /dev/null +++ b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml @@ -0,0 +1,4 @@ +--- +title: Added /duplicate slash command to close a duplicate issue +merge_request: +author: Ryan Scott diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 19b51c83222..ce4dd4e99d5 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -37,3 +37,4 @@ do. | `/target_branch ` | Set target branch for current merge request | | `/award :emoji:` | Toggle award for :emoji: | | `/board_move ~column` | Move issue to column on the board | +| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue | diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 1cd1f016674..d5de060b033 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -134,5 +134,46 @@ feature 'Issues > User uses quick actions', feature: true, js: true do expect(page).not_to have_content '/wip' end end + + describe 'mark issue as duplicate' do + let(:issue) { create(:issue, project: project) } + let(:original_issue) { create(:issue, project: project) } + + context 'when the current user can update issues' do + it 'does not create a note, and marks the issue as a duplicate' do + write_note("/duplicate ##{original_issue.to_reference}") + + expect(page).not_to have_content "/duplicate #{original_issue.to_reference}" + expect(page).to have_content 'Commands applied' + expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" + + issue.reload + + expect(issue.closed?).to be_truthy + end + end + + context 'when the current user cannot update the issue' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'does not create a note, and does not mark the issue as a duplicate' do + write_note("/duplicate ##{original_issue.to_reference}") + + expect(page).to have_content "/duplicate ##{original_issue.to_reference}" + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" + + issue.reload + + expect(issue.closed?).to be_falsey + end + end + end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index d0b991f19ab..3e7abf85106 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -491,6 +491,62 @@ describe Issues::UpdateService, services: true do include_examples 'updating mentions', Issues::UpdateService end + context 'duplicate issue' do + let(:issues_finder) { spy(:issues_finder) } + let(:close_service) { spy(:close_service) } + + before do + allow(IssuesFinder).to receive(:new).and_return(issues_finder) + allow(Issues::CloseService).to receive(:new).and_return(close_service) + allow(SystemNoteService).to receive(:cross_reference) + allow(SystemNoteService).to receive(:mark_duplicate_issue) + end + + context 'invalid original_issue_id' do + let(:original_issue_id) { double } + before { update_issue({ original_issue_id: original_issue_id }) } + + it 'finds the root issue' do + expect(issues_finder).to have_received(:find).with(original_issue_id) + end + + it 'does not close the issue' do + expect(close_service).not_to have_received(:execute) + end + + it 'does not create system notes' do + expect(SystemNoteService).not_to have_received(:cross_reference) + expect(SystemNoteService).not_to have_received(:mark_duplicate_issue) + end + end + + context 'valid original_issue_id' do + let(:original_issue) { create(:issue, project: project) } + let(:original_issue_id) { double } + + before do + allow(issues_finder).to receive(:find).and_return(original_issue) + update_issue({ original_issue_id: original_issue_id }) + end + + it 'finds the root issue' do + expect(issues_finder).to have_received(:find).with(original_issue_id) + end + + it 'closes the issue' do + expect(close_service).to have_received(:execute).with(issue) + end + + it 'creates a system note that this issue is a duplicate' do + expect(SystemNoteService).to have_received(:mark_duplicate_issue).with(issue, project, user, original_issue) + end + + it 'creates a cross reference system note in the other issue' do + expect(SystemNoteService).to have_received(:cross_reference).with(original_issue, issue, user) + end + end + end + include_examples 'issuable update service' do let(:open_issuable) { issue } let(:closed_issuable) { create(:closed_issue, project: project) } diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index a2db3f68ff7..3e4aa66756c 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -261,6 +261,17 @@ describe QuickActions::InterpretService, services: true do end end + shared_examples 'duplicate command' do + let(:issue_duplicate) { create(:issue, project: project) } + + it 'fetches issue and populates original_issue_id if content contains /duplicate issue_reference' do + issue_duplicate # populate the issue + _, updates = service.execute(content, issuable) + + expect(updates).to eq(original_issue_id: issue_duplicate.id) + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -644,6 +655,26 @@ describe QuickActions::InterpretService, services: true do let(:issuable) { issue } end + it_behaves_like 'duplicate command' do + let(:content) { "/duplicate #{issue_duplicate.to_reference}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate #{issue.to_reference}' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate imaginary#1234' } + let(:issuable) { issue } + end + context 'when current_user cannot :admin_issue' do let(:visitor) { create(:user) } let(:issue) { create(:issue, project: project, author: visitor) } @@ -693,6 +724,11 @@ describe QuickActions::InterpretService, services: true do let(:content) { '/remove_due_date' } let(:issuable) { issue } end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate #{issue.to_reference}' } + let(:issuable) { issue } + end end context '/award command' do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 60477b8e9ba..db120889119 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1101,4 +1101,29 @@ describe SystemNoteService, services: true do expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code)) end end + + describe '.mark_duplicate_issue' do + subject { described_class.mark_duplicate_issue(noteable, project, author, original_issue) } + + context 'within the same project' do + let(:original_issue) { create(:issue, project: project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked this issue as a duplicate of #{original_issue.to_reference}" } + end + + context 'across different projects' do + let(:other_project) { create(:empty_project) } + let(:original_issue) { create(:issue, project: other_project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked this issue as a duplicate of #{original_issue.to_reference(project)}" } + end + end end From 7e3d34595c3e090fe505b4fbd49cde2a303b1b6f Mon Sep 17 00:00:00 2001 From: Ryan Scott Date: Wed, 5 Apr 2017 11:31:48 +0900 Subject: [PATCH 046/143] Changes based on MR feedback. Marking an issue as a duplicate will now also add an upvote on behalf of the author on the original issue. --- app/models/system_note_metadata.rb | 5 +- app/services/issuable_base_service.rb | 20 +++---- app/services/system_note_service.rb | 8 +-- .../issues/user_uses_slash_commands_spec.rb | 8 +-- spec/services/issues/update_service_spec.rb | 48 ++++++----------- .../quick_actions/interpret_service_spec.rb | 52 +++++++++++++------ 6 files changed, 71 insertions(+), 70 deletions(-) diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 1ffdd285b91..0b33e45473b 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -1,8 +1,9 @@ class SystemNoteMetadata < ActiveRecord::Base ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference - title time_tracking branch milestone discussion task moved opened closed merged - outdated duplicate + title time_tracking branch milestone discussion task moved + opened closed merged duplicate + outdated ].freeze validates :note, presence: true diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index c7e646222bb..f57fbaca836 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -50,10 +50,6 @@ class IssuableBaseService < BaseService SystemNoteService.mark_duplicate_issue(issuable, issuable.project, current_user, original_issue) end - def create_cross_reference_note(noteable, mentioner) - SystemNoteService.cross_reference(noteable, mentioner, current_user) - end - def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" @@ -303,14 +299,18 @@ class IssuableBaseService < BaseService def change_issue_duplicate(issuable) original_issue_id = params.delete(:original_issue_id) - return if original_issue_id.nil? + return unless original_issue_id - original_issue = IssuesFinder.new(current_user).find(original_issue_id) - if original_issue.present? - create_issue_duplicate_note(issuable, original_issue) - close_service.new(project, current_user, {}).execute(issuable) - create_cross_reference_note(original_issue, issuable) + begin + original_issue = IssuesFinder.new(current_user).find(original_issue_id) + rescue ActiveRecord::RecordNotFound + return end + + note = create_issue_duplicate_note(issuable, original_issue) + note.create_cross_references! + close_service.new(project, current_user, {}).execute(issuable) + original_issue.create_award_emoji(AwardEmoji::UPVOTE_NAME, issuable.author) end def toggle_award(issuable) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 2e5e904c43d..ed079f0e495 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -552,11 +552,11 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'moved')) end - # Called when a Notable has been marked as a duplicate of another Issue + # Called when a Noteable has been marked as a duplicate of another Issue # - # noteable - Noteable object - # project - Project owning noteable - # author - User performing the change + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change # original_issue - Issue that this is a duplicate of # # Example Note text: diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index d5de060b033..28f27c76e35 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -147,9 +147,7 @@ feature 'Issues > User uses quick actions', feature: true, js: true do expect(page).to have_content 'Commands applied' expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" - issue.reload - - expect(issue.closed?).to be_truthy + expect(issue.reload).to be_closed end end @@ -169,9 +167,7 @@ feature 'Issues > User uses quick actions', feature: true, js: true do expect(page).not_to have_content 'Commands applied' expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" - issue.reload - - expect(issue.closed?).to be_falsey + expect(issue.reload).to be_open end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 3e7abf85106..e7f3ab93395 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -492,57 +492,43 @@ describe Issues::UpdateService, services: true do end context 'duplicate issue' do - let(:issues_finder) { spy(:issues_finder) } - let(:close_service) { spy(:close_service) } - - before do - allow(IssuesFinder).to receive(:new).and_return(issues_finder) - allow(Issues::CloseService).to receive(:new).and_return(close_service) - allow(SystemNoteService).to receive(:cross_reference) - allow(SystemNoteService).to receive(:mark_duplicate_issue) - end + let(:original_issue) { create(:issue, project: project) } context 'invalid original_issue_id' do - let(:original_issue_id) { double } - before { update_issue({ original_issue_id: original_issue_id }) } - - it 'finds the root issue' do - expect(issues_finder).to have_received(:find).with(original_issue_id) + before do + update_issue(original_issue_id: 123456789) end it 'does not close the issue' do - expect(close_service).not_to have_received(:execute) + expect(issue.reload).not_to be_closed end - it 'does not create system notes' do - expect(SystemNoteService).not_to have_received(:cross_reference) - expect(SystemNoteService).not_to have_received(:mark_duplicate_issue) + it 'does not create a system note' do + note = find_note("marked this issue as a duplicate of #{original_issue.to_reference}") + expect(note).to be_nil + end + + it 'does not upvote the issue on behalf of the author' do + expect(original_issue).not_to be_awarded_emoji(AwardEmoji::UPVOTE_NAME, issue.author) end end context 'valid original_issue_id' do - let(:original_issue) { create(:issue, project: project) } - let(:original_issue_id) { double } - before do - allow(issues_finder).to receive(:find).and_return(original_issue) - update_issue({ original_issue_id: original_issue_id }) - end - - it 'finds the root issue' do - expect(issues_finder).to have_received(:find).with(original_issue_id) + update_issue(original_issue_id: original_issue.id) end it 'closes the issue' do - expect(close_service).to have_received(:execute).with(issue) + expect(issue.reload).to be_closed end it 'creates a system note that this issue is a duplicate' do - expect(SystemNoteService).to have_received(:mark_duplicate_issue).with(issue, project, user, original_issue) + note = find_note("marked this issue as a duplicate of #{original_issue.to_reference}") + expect(note).not_to be_nil end - it 'creates a cross reference system note in the other issue' do - expect(SystemNoteService).to have_received(:cross_reference).with(original_issue, issue, user) + it 'upvotes the issue on behalf of the author' do + expect(original_issue).to be_awarded_emoji(AwardEmoji::UPVOTE_NAME, issue.author) end end end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 3e4aa66756c..1d60b74e566 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -262,8 +262,6 @@ describe QuickActions::InterpretService, services: true do end shared_examples 'duplicate command' do - let(:issue_duplicate) { create(:issue, project: project) } - it 'fetches issue and populates original_issue_id if content contains /duplicate issue_reference' do issue_duplicate # populate the issue _, updates = service.execute(content, issuable) @@ -655,24 +653,44 @@ describe QuickActions::InterpretService, services: true do let(:issuable) { issue } end - it_behaves_like 'duplicate command' do - let(:content) { "/duplicate #{issue_duplicate.to_reference}" } - let(:issuable) { issue } - end + context '/duplicate command' do + it_behaves_like 'duplicate command' do + let(:issue_duplicate) { create(:issue, project: project) } + let(:content) { "/duplicate #{issue_duplicate.to_reference}" } + let(:issuable) { issue } + end - it_behaves_like 'empty command' do - let(:content) { '/duplicate #{issue.to_reference}' } - let(:issuable) { issue } - end + it_behaves_like 'empty command' do + let(:content) { "/duplicate #{issue.to_reference}" } + let(:issuable) { issue } + end - it_behaves_like 'empty command' do - let(:content) { '/duplicate' } - let(:issuable) { issue } - end + it_behaves_like 'empty command' do + let(:content) { '/duplicate' } + let(:issuable) { issue } + end - it_behaves_like 'empty command' do - let(:content) { '/duplicate imaginary#1234' } - let(:issuable) { issue } + context 'cross project references' do + it_behaves_like 'duplicate command' do + let(:other_project) { create(:empty_project, :public) } + let(:issue_duplicate) { create(:issue, project: other_project) } + let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate imaginary#1234' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:other_project) { create(:empty_project, :private) } + let(:issue_duplicate) { create(:issue, project: other_project) } + + let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" } + let(:issuable) { issue } + end + end end context 'when current_user cannot :admin_issue' do From 3498e825d08adb0311d0431d9d15e450f95bfc86 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 18 Jul 2017 15:27:00 +0100 Subject: [PATCH 047/143] Fix feature specs --- spec/features/issues/user_uses_slash_commands_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 28f27c76e35..60b787fdd61 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -155,9 +155,9 @@ feature 'Issues > User uses quick actions', feature: true, js: true do let(:guest) { create(:user) } before do project.team << [guest, :guest] - logout - login_with(guest) - visit namespace_project_issue_path(project.namespace, project, issue) + gitlab_sign_out + sign_in(guest) + visit project_issue_path(project, issue) end it 'does not create a note, and does not mark the issue as a duplicate' do From de01b862254be634a0602c6a8875cdda0538354f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 21 Jul 2017 03:10:26 +0800 Subject: [PATCH 048/143] Add a note that schedules could be deactivated when lacking permissions too. --- doc/user/project/pipelines/schedules.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 258b3a2f955..9ad15a12c3c 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -71,9 +71,10 @@ The next time a pipeline is scheduled, your credentials will be used. >**Note:** When the owner of the schedule doesn't have the ability to create pipelines -anymore, due to e.g., being blocked or removed from the project, the schedule -is deactivated. Another user can take ownership and activate it, so the -schedule can be run again. +anymore, due to e.g., being blocked or removed from the project, or lacking +the permission to run on protected branches or tags. When this happened, the +schedule is deactivated. Another user can take ownership and activate it, so +the schedule can be run again. ## Advanced admin configuration From 8a444484345806dcbc0312d770b185edde1edb67 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 21 Jul 2017 17:49:32 +0800 Subject: [PATCH 049/143] Extract validations --- app/services/ci/create_pipeline_service.rb | 48 +++++++++++----------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 3ff698b6437..21e2ef153de 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -15,12 +15,34 @@ module Ci pipeline_schedule: schedule ) + result = validate(current_user || trigger_request.trigger.owner, + ignore_skip_ci: ignore_skip_ci, + save_on_errors: save_on_errors) + + return result if result + + Ci::Pipeline.transaction do + update_merge_requests_head_pipeline if pipeline.save + + Ci::CreatePipelineStagesService + .new(project, current_user) + .execute(pipeline) + end + + cancel_pending_pipelines if project.auto_cancel_pending_pipelines? + + pipeline_created_counter.increment(source: source) + + pipeline.tap(&:process!) + end + + private + + def validate(triggering_user, ignore_skip_ci:, save_on_errors:) unless project.builds_enabled? return error('Pipeline is disabled') end - triggering_user = current_user || trigger_request.trigger.owner - unless allowed_to_trigger_pipeline?(triggering_user) if can?(triggering_user, :create_pipeline, project) return error("Insufficient permissions for protected ref '#{ref}'") @@ -52,28 +74,6 @@ module Ci unless pipeline.has_stage_seeds? return error('No stages / jobs for this pipeline.') end - - process! do - pipeline_created_counter.increment(source: source) - end - end - - private - - def process! - Ci::Pipeline.transaction do - update_merge_requests_head_pipeline if pipeline.save - - Ci::CreatePipelineStagesService - .new(project, current_user) - .execute(pipeline) - end - - cancel_pending_pipelines if project.auto_cancel_pending_pipelines? - - yield - - pipeline.tap(&:process!) end def allowed_to_trigger_pipeline?(triggering_user) From eaa935d77b824510a141ab10e9471107c516f902 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 21 Jul 2017 13:09:13 +0200 Subject: [PATCH 050/143] Fix target project merge request link on build page --- app/serializers/build_details_entity.rb | 3 ++- spec/serializers/build_details_entity_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 8ad5af1987c..743a08acefe 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -16,7 +16,8 @@ class BuildDetailsEntity < JobEntity end expose :path do |build| - project_merge_request_path(build.project, build.merge_request) + project_merge_request_path(build.merge_request.project, + build.merge_request) end end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 446a2451956..1332572fffc 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -81,9 +81,9 @@ describe BuildDetailsEntity do expect(subject[:merge_request][:iid]).to eq merge_request.iid end - it 'has a correct merge request path' do + it 'has a merge request path to a target project' do expect(subject[:merge_request][:path]) - .to include fork_project.full_path + .to include project.full_path end end From 1df696f5a6836e03a6bf8d5139c2c7ce6d96e727 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 20 Jul 2017 15:42:33 +0100 Subject: [PATCH 051/143] Move duplicate issue management to a service --- app/helpers/system_note_helper.rb | 3 +- app/services/issuable_base_service.rb | 23 +----- app/services/issues/base_service.rb | 8 ++ app/services/issues/duplicate_service.rb | 24 ++++++ app/services/issues/update_service.rb | 18 +++-- .../quick_actions/interpret_service.rb | 10 ++- app/services/system_note_service.rb | 31 +++++-- app/views/shared/icons/_icon_clone.svg | 3 + .../26372-duplicate-issue-slash-command.yml | 4 +- .../services/issues/duplicate_service_spec.rb | 80 +++++++++++++++++++ spec/services/issues/update_service_spec.rb | 41 +++------- .../quick_actions/interpret_service_spec.rb | 11 +-- spec/services/system_note_service_spec.rb | 35 ++++++-- 13 files changed, 205 insertions(+), 86 deletions(-) create mode 100644 app/services/issues/duplicate_service.rb create mode 100644 app/views/shared/icons/_icon_clone.svg create mode 100644 spec/services/issues/duplicate_service_spec.rb diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index 209bd56b78a..08fd97cd048 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -18,7 +18,8 @@ module SystemNoteHelper 'milestone' => 'icon_clock_o', 'discussion' => 'icon_comment_o', 'moved' => 'icon_arrow_circle_o_right', - 'outdated' => 'icon_edit' + 'outdated' => 'icon_edit', + 'duplicate' => 'icon_clone' }.freeze def icon_for_system_note(note) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index f57fbaca836..ea497729115 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -46,10 +46,6 @@ class IssuableBaseService < BaseService SystemNoteService.change_time_spent(issuable, issuable.project, current_user) end - def create_issue_duplicate_note(issuable, original_issue) - SystemNoteService.mark_duplicate_issue(issuable, issuable.project, current_user, original_issue) - end - def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" @@ -62,7 +58,7 @@ class IssuableBaseService < BaseService params.delete(:assignee_ids) params.delete(:assignee_id) params.delete(:due_date) - params.delete(:original_issue_id) + params.delete(:canonical_issue_id) end filter_assignee(issuable) @@ -214,7 +210,6 @@ class IssuableBaseService < BaseService change_state(issuable) change_subscription(issuable) change_todo(issuable) - change_issue_duplicate(issuable) toggle_award(issuable) filter_params(issuable) old_labels = issuable.labels.to_a @@ -297,22 +292,6 @@ class IssuableBaseService < BaseService end end - def change_issue_duplicate(issuable) - original_issue_id = params.delete(:original_issue_id) - return unless original_issue_id - - begin - original_issue = IssuesFinder.new(current_user).find(original_issue_id) - rescue ActiveRecord::RecordNotFound - return - end - - note = create_issue_duplicate_note(issuable, original_issue) - note.create_cross_references! - close_service.new(project, current_user, {}).execute(issuable) - original_issue.create_award_emoji(AwardEmoji::UPVOTE_NAME, issuable.author) - end - def toggle_award(issuable) award = params.delete(:emoji_award) if award diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 34199eb5d13..4c198fc96ea 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -7,6 +7,14 @@ module Issues issue_data end + def reopen_service + Issues::ReopenService + end + + def close_service + Issues::CloseService + end + private def create_assignee_note(issue, old_assignees) diff --git a/app/services/issues/duplicate_service.rb b/app/services/issues/duplicate_service.rb new file mode 100644 index 00000000000..5c0854e664d --- /dev/null +++ b/app/services/issues/duplicate_service.rb @@ -0,0 +1,24 @@ +module Issues + class DuplicateService < Issues::BaseService + def execute(duplicate_issue, canonical_issue) + return if canonical_issue == duplicate_issue + return unless can?(current_user, :update_issue, duplicate_issue) + return unless can?(current_user, :create_note, canonical_issue) + + create_issue_duplicate_note(duplicate_issue, canonical_issue) + create_issue_canonical_note(canonical_issue, duplicate_issue) + + close_service.new(project, current_user, {}).execute(duplicate_issue) + end + + private + + def create_issue_duplicate_note(duplicate_issue, canonical_issue) + SystemNoteService.mark_duplicate_issue(duplicate_issue, duplicate_issue.project, current_user, canonical_issue) + end + + def create_issue_canonical_note(canonical_issue, duplicate_issue) + SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue) + end + end +end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index cd9f9a4a16e..8d918ccc635 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -5,6 +5,7 @@ module Issues def execute(issue) handle_move_between_iids(issue) filter_spam_check_params + change_issue_duplicate(issue) update(issue) end @@ -53,14 +54,6 @@ module Issues end end - def reopen_service - Issues::ReopenService - end - - def close_service - Issues::CloseService - end - def handle_move_between_iids(issue) return unless params[:move_between_iids] @@ -72,6 +65,15 @@ module Issues issue.move_between(issue_before, issue_after) end + def change_issue_duplicate(issue) + canonical_issue_id = params.delete(:canonical_issue_id) + canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id) + + if canonical_issue + Issues::DuplicateService.new(project, current_user).execute(issue, canonical_issue) + end + end + private def get_issue_if_allowed(project, iid) diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 3eecf0b5545..5dc1b91d2c0 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -472,6 +472,9 @@ module QuickActions end desc 'Mark this issue as a duplicate of another issue' + explanation do |duplicate_reference| + "Marks this issue as a duplicate of #{duplicate_reference}." + end params '#issue' condition do issuable.is_a?(Issue) && @@ -479,9 +482,10 @@ module QuickActions current_user.can?(:"update_#{issuable.to_ability_name}", issuable) end command :duplicate do |duplicate_param| - original_issue = extract_references(duplicate_param, :issue).first - if original_issue.present? && original_issue != issuable - @updates[:original_issue_id] = original_issue.id + canonical_issue = extract_references(duplicate_param, :issue).first + + if canonical_issue.present? + @updates[:canonical_issue_id] = canonical_issue.id end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index ed079f0e495..2dbee9c246e 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -554,10 +554,10 @@ module SystemNoteService # Called when a Noteable has been marked as a duplicate of another Issue # - # noteable - Noteable object - # project - Project owning noteable - # author - User performing the change - # original_issue - Issue that this is a duplicate of + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # canonical_issue - Issue that this is a duplicate of # # Example Note text: # @@ -566,8 +566,27 @@ module SystemNoteService # "marked this issue as a duplicate of other_project#5678" # # Returns the created Note object - def mark_duplicate_issue(noteable, project, author, original_issue) - body = "marked this issue as a duplicate of #{original_issue.to_reference(project)}" + def mark_duplicate_issue(noteable, project, author, canonical_issue) + body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" + create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) + end + + # Called when a Noteable has been marked as the canonical Issue of a duplicate + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # duplicate_issue - Issue that was a duplicate of this + # + # Example Note text: + # + # "marked #1234 as a duplicate of this issue" + # + # "marked other_project#5678 as a duplicate of this issue" + # + # Returns the created Note object + def mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue) + body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) end diff --git a/app/views/shared/icons/_icon_clone.svg b/app/views/shared/icons/_icon_clone.svg new file mode 100644 index 00000000000..ccc897aa98f --- /dev/null +++ b/app/views/shared/icons/_icon_clone.svg @@ -0,0 +1,3 @@ + + + diff --git a/changelogs/unreleased/26372-duplicate-issue-slash-command.yml b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml index 079ebe59f98..3108344e0bf 100644 --- a/changelogs/unreleased/26372-duplicate-issue-slash-command.yml +++ b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml @@ -1,4 +1,4 @@ --- -title: Added /duplicate slash command to close a duplicate issue -merge_request: +title: Added /duplicate quick action to close a duplicate issue +merge_request: 12845 author: Ryan Scott diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb new file mode 100644 index 00000000000..82daf53b173 --- /dev/null +++ b/spec/services/issues/duplicate_service_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Issues::DuplicateService, services: true do + let(:user) { create(:user) } + let(:canonical_project) { create(:empty_project) } + let(:duplicate_project) { create(:empty_project) } + + let(:canonical_issue) { create(:issue, project: canonical_project) } + let(:duplicate_issue) { create(:issue, project: duplicate_project) } + + subject { described_class.new(duplicate_project, user, {}) } + + describe '#execute' do + context 'when the issues passed are the same' do + it 'does nothing' do + expect(subject).not_to receive(:close_service) + expect(SystemNoteService).not_to receive(:mark_duplicate_issue) + expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate) + + subject.execute(duplicate_issue, duplicate_issue) + end + end + + context 'when the user cannot update the duplicate issue' do + before do + canonical_project.add_reporter(user) + end + + it 'does nothing' do + expect(subject).not_to receive(:close_service) + expect(SystemNoteService).not_to receive(:mark_duplicate_issue) + expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate) + + subject.execute(duplicate_issue, canonical_issue) + end + end + + context 'when the user cannot comment on the canonical issue' do + before do + duplicate_project.add_reporter(user) + end + + it 'does nothing' do + expect(subject).not_to receive(:close_service) + expect(SystemNoteService).not_to receive(:mark_duplicate_issue) + expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate) + + subject.execute(duplicate_issue, canonical_issue) + end + end + + context 'when the user can mark the issue as a duplicate' do + before do + canonical_project.add_reporter(user) + duplicate_project.add_reporter(user) + end + + it 'closes the duplicate issue' do + subject.execute(duplicate_issue, canonical_issue) + + expect(duplicate_issue.reload).to be_closed + expect(canonical_issue.reload).to be_open + end + + it 'adds a system note to the duplicate issue' do + expect(SystemNoteService) + .to receive(:mark_duplicate_issue).with(duplicate_issue, duplicate_project, user, canonical_issue) + + subject.execute(duplicate_issue, canonical_issue) + end + + it 'adds a system note to the canonical issue' do + expect(SystemNoteService) + .to receive(:mark_canonical_issue_of_duplicate).with(canonical_issue, canonical_project, user, duplicate_issue) + + subject.execute(duplicate_issue, canonical_issue) + end + end + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index e7f3ab93395..064be940a1c 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -492,43 +492,22 @@ describe Issues::UpdateService, services: true do end context 'duplicate issue' do - let(:original_issue) { create(:issue, project: project) } + let(:canonical_issue) { create(:issue, project: project) } - context 'invalid original_issue_id' do - before do - update_issue(original_issue_id: 123456789) - end + context 'invalid canonical_issue_id' do + it 'does not call the duplicate service' do + expect(Issues::DuplicateService).not_to receive(:new) - it 'does not close the issue' do - expect(issue.reload).not_to be_closed - end - - it 'does not create a system note' do - note = find_note("marked this issue as a duplicate of #{original_issue.to_reference}") - expect(note).to be_nil - end - - it 'does not upvote the issue on behalf of the author' do - expect(original_issue).not_to be_awarded_emoji(AwardEmoji::UPVOTE_NAME, issue.author) + update_issue(canonical_issue_id: 123456789) end end - context 'valid original_issue_id' do - before do - update_issue(original_issue_id: original_issue.id) - end + context 'valid canonical_issue_id' do + it 'calls the duplicate service with both issues' do + expect_any_instance_of(Issues::DuplicateService) + .to receive(:execute).with(issue, canonical_issue) - it 'closes the issue' do - expect(issue.reload).to be_closed - end - - it 'creates a system note that this issue is a duplicate' do - note = find_note("marked this issue as a duplicate of #{original_issue.to_reference}") - expect(note).not_to be_nil - end - - it 'upvotes the issue on behalf of the author' do - expect(original_issue).to be_awarded_emoji(AwardEmoji::UPVOTE_NAME, issue.author) + update_issue(canonical_issue_id: canonical_issue.id) end end end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 1d60b74e566..2a2a5c38e4b 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -262,11 +262,11 @@ describe QuickActions::InterpretService, services: true do end shared_examples 'duplicate command' do - it 'fetches issue and populates original_issue_id if content contains /duplicate issue_reference' do + it 'fetches issue and populates canonical_issue_id if content contains /duplicate issue_reference' do issue_duplicate # populate the issue _, updates = service.execute(content, issuable) - expect(updates).to eq(original_issue_id: issue_duplicate.id) + expect(updates).to eq(canonical_issue_id: issue_duplicate.id) end end @@ -660,11 +660,6 @@ describe QuickActions::InterpretService, services: true do let(:issuable) { issue } end - it_behaves_like 'empty command' do - let(:content) { "/duplicate #{issue.to_reference}" } - let(:issuable) { issue } - end - it_behaves_like 'empty command' do let(:content) { '/duplicate' } let(:issuable) { issue } @@ -679,7 +674,7 @@ describe QuickActions::InterpretService, services: true do end it_behaves_like 'empty command' do - let(:content) { '/duplicate imaginary#1234' } + let(:content) { "/duplicate imaginary#1234" } let(:issuable) { issue } end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index db120889119..681b419aedf 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1103,27 +1103,52 @@ describe SystemNoteService, services: true do end describe '.mark_duplicate_issue' do - subject { described_class.mark_duplicate_issue(noteable, project, author, original_issue) } + subject { described_class.mark_duplicate_issue(noteable, project, author, canonical_issue) } context 'within the same project' do - let(:original_issue) { create(:issue, project: project) } + let(:canonical_issue) { create(:issue, project: project) } it_behaves_like 'a system note' do let(:action) { 'duplicate' } end - it { expect(subject.note).to eq "marked this issue as a duplicate of #{original_issue.to_reference}" } + it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference}" } end context 'across different projects' do let(:other_project) { create(:empty_project) } - let(:original_issue) { create(:issue, project: other_project) } + let(:canonical_issue) { create(:issue, project: other_project) } it_behaves_like 'a system note' do let(:action) { 'duplicate' } end - it { expect(subject.note).to eq "marked this issue as a duplicate of #{original_issue.to_reference(project)}" } + it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" } + end + end + + describe '.mark_canonical_issue_of_duplicate' do + subject { described_class.mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue) } + + context 'within the same project' do + let(:duplicate_issue) { create(:issue, project: project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference} as a duplicate of this issue" } + end + + context 'across different projects' do + let(:other_project) { create(:empty_project) } + let(:duplicate_issue) { create(:issue, project: other_project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" } end end end From c5c9dce270516adf3a2e4a549d1c32b6a3223335 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 12 Jul 2017 16:58:48 -0300 Subject: [PATCH 052/143] Add group milestones API endpoint --- app/finders/issuable_finder.rb | 2 +- doc/api/README.md | 3 +- doc/api/group_milestones.md | 120 +++++++++++ lib/api/api.rb | 3 +- lib/api/entities.rb | 4 +- lib/api/group_milestones.rb | 85 ++++++++ lib/api/helpers.rb | 4 + lib/api/milestone_responses.rb | 98 +++++++++ lib/api/milestones.rb | 154 -------------- lib/api/project_milestones.rb | 91 +++++++++ spec/requests/api/group_milestones_spec.rb | 21 ++ spec/requests/api/project_milestones_spec.rb | 25 +++ .../api/milestones_shared_examples.rb} | 190 +++++++++--------- 13 files changed, 545 insertions(+), 255 deletions(-) create mode 100644 doc/api/group_milestones.md create mode 100644 lib/api/group_milestones.rb create mode 100644 lib/api/milestone_responses.rb delete mode 100644 lib/api/milestones.rb create mode 100644 lib/api/project_milestones.rb create mode 100644 spec/requests/api/group_milestones_spec.rb create mode 100644 spec/requests/api/project_milestones_spec.rb rename spec/{requests/api/milestones_spec.rb => support/api/milestones_shared_examples.rb} (63%) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 2e5a6493134..762c0861cd2 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -20,7 +20,7 @@ # class IssuableFinder include CreatedAtFilter - + NONE = '0'.freeze IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze diff --git a/doc/api/README.md b/doc/api/README.md index 95e7a457848..a888c0ebb4e 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -29,7 +29,8 @@ following locations: - [Keys](keys.md) - [Labels](labels.md) - [Merge Requests](merge_requests.md) -- [Milestones](milestones.md) +- [Project milestones](milestones.md) +- [Group milestones](group_milestones.md) - [Namespaces](namespaces.md) - [Notes](notes.md) (comments) - [Notification settings](notification_settings.md) diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md new file mode 100644 index 00000000000..086fba7e91d --- /dev/null +++ b/doc/api/group_milestones.md @@ -0,0 +1,120 @@ +# Group milestones API + +## List group milestones + +Returns a list of group milestones. + +``` +GET /groups/:id/milestones +GET /groups/:id/milestones?iids=42 +GET /groups/:id/milestones?iids[]=42&iids[]=43 +GET /groups/:id/milestones?state=active +GET /groups/:id/milestones?state=closed +GET /groups/:id/milestones?search=version +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` | +| `state` | string | optional | Return only `active` or `closed` milestones` | +| `search` | string | optional | Return only milestones with a title or description matching the provided string | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/milestones +``` + +Example Response: + +```json +[ + { + "id": 12, + "iid": 3, + "group_id": 16, + "title": "10.0", + "description": "Version", + "due_date": "2013-11-29", + "start_date": "2013-11-10", + "state": "active", + "updated_at": "2013-10-02T09:24:18Z", + "created_at": "2013-10-02T09:24:18Z" + } +] +``` + + +## Get single milestone + +Gets a single group milestone. + +``` +GET /groups/:id/milestones/:milestone_id +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user +- `milestone_id` (required) - The ID of the group milestone + +## Create new milestone + +Creates a new group milestone. + +``` +POST /groups/:id/milestones +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user +- `title` (required) - The title of an milestone +- `description` (optional) - The description of the milestone +- `due_date` (optional) - The due date of the milestone +- `start_date` (optional) - The start date of the milestone + +## Edit milestone + +Updates an existing group milestone. + +``` +PUT /groups/:id/milestones/:milestone_id +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user +- `milestone_id` (required) - The ID of a group milestone +- `title` (optional) - The title of a milestone +- `description` (optional) - The description of a milestone +- `due_date` (optional) - The due date of the milestone +- `start_date` (optional) - The start date of the milestone +- `state_event` (optional) - The state event of the milestone (close|activate) + +## Get all issues assigned to a single milestone + +Gets all issues assigned to a single group milestone. + +``` +GET /groups/:id/milestones/:milestone_id/issues +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user +- `milestone_id` (required) - The ID of a group milestone + +## Get all merge requests assigned to a single milestone + +Gets all merge requests assigned to a single group milestone. + +``` +GET /groups/:id/milestones/:milestone_id/merge_requests +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user +- `milestone_id` (required) - The ID of a group milestone diff --git a/lib/api/api.rb b/lib/api/api.rb index efcf0976a81..7e45c34731f 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -109,7 +109,8 @@ module API mount ::API::Members mount ::API::MergeRequestDiffs mount ::API::MergeRequests - mount ::API::Milestones + mount ::API::ProjectMilestones + mount ::API::GroupMilestones mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 09a88869063..586325ddb0c 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -269,8 +269,8 @@ module API class Milestone < Grape::Entity expose :id, :iid - expose(:project_id) { |entity| entity&.project_id } - expose(:group_id) { |entity| entity&.group_id } + expose :project_id, if: -> (entity, options) { entity&.project_id } + expose :group_id, if: -> (entity, options) { entity&.group_id } expose :title, :description expose :state, :created_at, :updated_at expose :due_date diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb new file mode 100644 index 00000000000..b85eb59dc0a --- /dev/null +++ b/lib/api/group_milestones.rb @@ -0,0 +1,85 @@ +module API + class GroupMilestones < Grape::API + include MilestoneResponses + include PaginationParams + + before do + authenticate! + end + + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: { id: %r{[^/]+} } do + desc 'Get a list of group milestones' do + success Entities::Milestone + end + params do + use :list_params + end + get ":id/milestones" do + list_milestones_for(user_group) + end + + desc 'Get a single group milestone' do + success Entities::Milestone + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a group milestone' + end + get ":id/milestones/:milestone_id" do + authorize! :read_group, user_group + + get_milestone_for(user_group) + end + + desc 'Create a new group milestone' do + success Entities::Milestone + end + params do + requires :title, type: String, desc: 'The title of the milestone' + use :optional_params + end + post ":id/milestones" do + authorize! :admin_milestones, user_group + + create_milestone_for(user_group) + end + + desc 'Update an existing group milestone' do + success Entities::Milestone + end + params do + use :update_params + end + put ":id/milestones/:milestone_id" do + authorize! :admin_milestones, user_group + + update_milestone_for(user_group) + end + + desc 'Get all issues for a single group milestone' do + success Entities::IssueBasic + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a group milestone' + use :pagination + end + get ":id/milestones/:milestone_id/issues" do + milestone_issuables_for(user_group, :issue) + end + + desc 'Get all merge requests for a single group milestone' do + detail 'This feature was introduced in GitLab 9.' + success Entities::MergeRequestBasic + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a group milestone' + use :pagination + end + get ':id/milestones/:milestone_id/merge_requests' do + milestone_issuables_for(user_group, :merge_request) + end + end + end +end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0f4791841d2..57e3e93500f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -25,6 +25,10 @@ module API initial_current_user != current_user end + def user_group + @group ||= find_group!(params[:id]) + end + def user_project @project ||= find_project!(params[:id]) end diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb new file mode 100644 index 00000000000..ef09d9505d2 --- /dev/null +++ b/lib/api/milestone_responses.rb @@ -0,0 +1,98 @@ +module API + module MilestoneResponses + extend ActiveSupport::Concern + + included do + helpers do + params :optional_params do + optional :description, type: String, desc: 'The description of the milestone' + optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)' + optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)' + end + + params :list_params do + optional :state, type: String, values: %w[active closed all], default: 'all', + desc: 'Return "active", "closed", or "all" milestones' + optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' + optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' + use :pagination + end + + params :update_params do + requires :milestone_id, type: Integer, desc: 'The milestone ID number' + optional :title, type: String, desc: 'The title of the milestone' + optional :state_event, type: String, values: %w[close activate], + desc: 'The state event of the milestone ' + use :optional_params + at_least_one_of :title, :description, :due_date, :state_event + end + + def list_milestones_for(parent) + milestones = parent.milestones + milestones = Milestone.filter_by_state(milestones, params[:state]) + milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present? + milestones = filter_by_search(milestones, params[:search]) if params[:search] + + present paginate(milestones), with: Entities::Milestone + end + + def get_milestone_for(parent) + milestone = parent.milestones.find(params[:milestone_id]) + present milestone, with: Entities::Milestone + end + + def create_milestone_for(parent) + milestone = ::Milestones::CreateService.new(parent, current_user, declared_params).execute + + if milestone.valid? + present milestone, with: Entities::Milestone + else + render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400) + end + end + + def update_milestone_for(parent) + milestone = parent.milestones.find(params.delete(:milestone_id)) + + milestone_params = declared_params(include_missing: false) + milestone = ::Milestones::UpdateService.new(parent, current_user, milestone_params).execute(milestone) + + if milestone.valid? + present milestone, with: Entities::Milestone + else + render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400) + end + end + + def milestone_issuables_for(parent, type) + milestone = parent.milestones.find(params[:milestone_id]) + + finder_klass, entity = get_finder_and_entity(type) + + params = build_finder_params(milestone, parent) + + issuables = finder_klass.new(current_user, params).execute + present paginate(issuables), with: entity, current_user: current_user + end + + def build_finder_params(milestone, parent) + finder_params = { milestone_title: milestone.title, sort: 'label_priority' } + + if parent.is_a?(Group) + finder_params.merge(group_id: parent.id) + else + finder_params.merge(project_id: parent.id) + end + end + + def get_finder_and_entity(type) + if type == :issue + [IssuesFinder, Entities::IssueBasic] + else + [MergeRequestsFinder, Entities::MergeRequestBasic] + end + end + end + end + end +end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb deleted file mode 100644 index 3541d3c95fb..00000000000 --- a/lib/api/milestones.rb +++ /dev/null @@ -1,154 +0,0 @@ -module API - class Milestones < Grape::API - include PaginationParams - - before { authenticate! } - - helpers do - def filter_milestones_state(milestones, state) - case state - when 'active' then milestones.active - when 'closed' then milestones.closed - else milestones - end - end - - params :optional_params do - optional :description, type: String, desc: 'The description of the milestone' - optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)' - optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)' - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a list of project milestones' do - success Entities::Milestone - end - params do - optional :state, type: String, values: %w[active closed all], default: 'all', - desc: 'Return "active", "closed", or "all" milestones' - optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' - optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' - use :pagination - end - get ":id/milestones" do - authorize! :read_milestone, user_project - - milestones = user_project.milestones - milestones = filter_milestones_state(milestones, params[:state]) - milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present? - milestones = filter_by_search(milestones, params[:search]) if params[:search] - - present paginate(milestones), with: Entities::Milestone - end - - desc 'Get a single project milestone' do - success Entities::Milestone - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - end - get ":id/milestones/:milestone_id" do - authorize! :read_milestone, user_project - - milestone = user_project.milestones.find(params[:milestone_id]) - present milestone, with: Entities::Milestone - end - - desc 'Create a new project milestone' do - success Entities::Milestone - end - params do - requires :title, type: String, desc: 'The title of the milestone' - use :optional_params - end - post ":id/milestones" do - authorize! :admin_milestone, user_project - - milestone = ::Milestones::CreateService.new(user_project, current_user, declared_params).execute - - if milestone.valid? - present milestone, with: Entities::Milestone - else - render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400) - end - end - - desc 'Update an existing project milestone' do - success Entities::Milestone - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - optional :title, type: String, desc: 'The title of the milestone' - optional :state_event, type: String, values: %w[close activate], - desc: 'The state event of the milestone ' - use :optional_params - at_least_one_of :title, :description, :due_date, :state_event - end - put ":id/milestones/:milestone_id" do - authorize! :admin_milestone, user_project - milestone = user_project.milestones.find(params.delete(:milestone_id)) - - milestone_params = declared_params(include_missing: false) - milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone) - - if milestone.valid? - present milestone, with: Entities::Milestone - else - render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400) - end - end - - desc 'Get all issues for a single project milestone' do - success Entities::IssueBasic - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - use :pagination - end - get ":id/milestones/:milestone_id/issues" do - authorize! :read_milestone, user_project - - milestone = user_project.milestones.find(params[:milestone_id]) - - finder_params = { - project_id: user_project.id, - milestone_title: milestone.title, - sort: 'label_priority' - } - - issues = IssuesFinder.new(current_user, finder_params).execute - present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project - end - - desc 'Get all merge requests for a single project milestone' do - detail 'This feature was introduced in GitLab 9.' - success Entities::MergeRequestBasic - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - use :pagination - end - get ':id/milestones/:milestone_id/merge_requests' do - authorize! :read_milestone, user_project - - milestone = user_project.milestones.find(params[:milestone_id]) - - finder_params = { - project_id: user_project.id, - milestone_title: milestone.title, - sort: 'label_priority' - } - - merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute - present paginate(merge_requests), - with: Entities::MergeRequestBasic, - current_user: current_user, - project: user_project - end - end - end -end diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb new file mode 100644 index 00000000000..451998c726a --- /dev/null +++ b/lib/api/project_milestones.rb @@ -0,0 +1,91 @@ +module API + class ProjectMilestones < Grape::API + include PaginationParams + include MilestoneResponses + + before do + authenticate! + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get a list of project milestones' do + success Entities::Milestone + end + params do + use :list_params + end + get ":id/milestones" do + authorize! :read_milestone, user_project + + list_milestones_for(user_project) + end + + desc 'Get a single project milestone' do + success Entities::Milestone + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + end + get ":id/milestones/:milestone_id" do + authorize! :read_milestone, user_project + + get_milestone_for(user_project) + end + + desc 'Create a new project milestone' do + success Entities::Milestone + end + params do + requires :title, type: String, desc: 'The title of the milestone' + use :optional_params + end + post ":id/milestones" do + authorize! :admin_milestone, user_project + + create_milestone_for(user_project) + end + + desc 'Update an existing project milestone' do + success Entities::Milestone + end + params do + use :update_params + end + put ":id/milestones/:milestone_id" do + authorize! :admin_milestone, user_project + + update_milestone_for(user_project) + end + + desc 'Get all issues for a single project milestone' do + success Entities::IssueBasic + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + use :pagination + end + get ":id/milestones/:milestone_id/issues" do + authorize! :read_milestone, user_project + + milestone_issuables_for(user_project, :issue) + end + + desc 'Get all merge requests for a single project milestone' do + detail 'This feature was introduced in GitLab 9.' + success Entities::MergeRequestBasic + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + use :pagination + end + get ':id/milestones/:milestone_id/merge_requests' do + authorize! :read_milestone, user_project + + milestone_issuables_for(user_project, :merge_request) + end + end + end +end diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb new file mode 100644 index 00000000000..9b24658771f --- /dev/null +++ b/spec/requests/api/group_milestones_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe API::GroupMilestones do + let(:user) { create(:user) } + let(:group) { create(:group, :private) } + let(:project) { create(:empty_project, namespace: group) } + let!(:group_member) { create(:group_member, group: group, user: user) } + let!(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') } + let!(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') } + + it_behaves_like 'group and project milestones', "/groups/:id/milestones" do + let(:route) { "/groups/#{group.id}/milestones" } + end + + def setup_for_group + context_group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + context_group.add_developer(user) + public_project.update(namespace: context_group) + context_group.reload + end +end diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb new file mode 100644 index 00000000000..fe8fdbfd7e4 --- /dev/null +++ b/spec/requests/api/project_milestones_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe API::ProjectMilestones do + let(:user) { create(:user) } + let!(:project) { create(:empty_project, namespace: user.namespace ) } + let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } + let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } + + before do + project.team << [user, :developer] + end + + it_behaves_like 'group and project milestones', "/projects/:id/milestones" do + let(:route) { "/projects/#{project.id}/milestones" } + end + + describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do + it 'creates an activity event when an milestone is closed' do + expect(Event).to receive(:create) + + put api("/projects/#{project.id}/milestones/#{milestone.id}", user), + state_event: 'close' + end + end +end diff --git a/spec/requests/api/milestones_spec.rb b/spec/support/api/milestones_shared_examples.rb similarity index 63% rename from spec/requests/api/milestones_spec.rb rename to spec/support/api/milestones_shared_examples.rb index ab5ea3e8f2c..480e7d5151f 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -1,21 +1,14 @@ -require 'spec_helper' - -describe API::Milestones do - let(:user) { create(:user) } - let!(:project) { create(:empty_project, namespace: user.namespace ) } - let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } - let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } +shared_examples_for 'group and project milestones' do |route_definition| + let(:resource_route) { "#{route}/#{milestone.id}" } let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } let(:label_3) { create(:label, title: 'label_3', project: project) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:another_merge_request) { create(:merge_request, :simple, source_project: project) } - before do - project.team << [user, :developer] - end - - describe 'GET /projects/:id/milestones' do - it 'returns project milestones' do - get api("/projects/#{project.id}/milestones", user) + describe "GET #{route_definition}" do + it 'returns milestones list' do + get api(route, user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -24,13 +17,13 @@ describe API::Milestones do end it 'returns a 401 error if user not authenticated' do - get api("/projects/#{project.id}/milestones") + get api(route) expect(response).to have_http_status(401) end it 'returns an array of active milestones' do - get api("/projects/#{project.id}/milestones?state=active", user) + get api("#{route}/?state=active", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -40,7 +33,7 @@ describe API::Milestones do end it 'returns an array of closed milestones' do - get api("/projects/#{project.id}/milestones?state=closed", user) + get api("#{route}/?state=closed", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -50,9 +43,9 @@ describe API::Milestones do end it 'returns an array of milestones specified by iids' do - other_milestone = create(:milestone, project: project) + other_milestone = create(:milestone, project: try(:project), group: try(:group)) - get api("/projects/#{project.id}/milestones", user), iids: [closed_milestone.iid, other_milestone.iid] + get api(route, user), iids: [closed_milestone.iid, other_milestone.iid] expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -61,25 +54,15 @@ describe API::Milestones do end it 'does not return any milestone if none found' do - get api("/projects/#{project.id}/milestones", user), iids: [Milestone.maximum(:iid).succ] + get api(route, user), iids: [Milestone.maximum(:iid).succ] expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end - end - describe 'GET /projects/:id/milestones/:milestone_id' do - it 'returns a project milestone by id' do - get api("/projects/#{project.id}/milestones/#{milestone.id}", user) - - expect(response).to have_http_status(200) - expect(json_response['title']).to eq(milestone.title) - expect(json_response['iid']).to eq(milestone.iid) - end - - it 'returns a project milestone by iids array' do - get api("/projects/#{project.id}/milestones?iids=#{closed_milestone.iid}", user) + it 'returns a milestone by iids array' do + get api("#{route}?iids=#{closed_milestone.iid}", user) expect(response.status).to eq 200 expect(response).to include_pagination_headers @@ -89,8 +72,8 @@ describe API::Milestones do expect(json_response.first['id']).to eq closed_milestone.id end - it 'returns a project milestone by searching for title' do - get api("/projects/#{project.id}/milestones", user), search: 'version2' + it 'returns a milestone by searching for title' do + get api(route, user), search: 'version2' expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -99,8 +82,8 @@ describe API::Milestones do expect(json_response.first['id']).to eq milestone.id end - it 'returns a project milestones by searching for description' do - get api("/projects/#{project.id}/milestones", user), search: 'open' + it 'returns a milestones by searching for description' do + get api(route, user), search: 'open' expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -110,9 +93,17 @@ describe API::Milestones do end end - describe 'GET /projects/:id/milestones/:milestone_id' do - it 'returns a project milestone by id' do - get api("/projects/#{project.id}/milestones/#{milestone.id}", user) + describe "GET #{route_definition}/:milestone_id" do + it 'returns a milestone by id' do + get api(resource_route, user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) + end + + it 'returns a milestone by id' do + get api(resource_route, user) expect(response).to have_http_status(200) expect(json_response['title']).to eq(milestone.title) @@ -120,29 +111,29 @@ describe API::Milestones do end it 'returns 401 error if user not authenticated' do - get api("/projects/#{project.id}/milestones/#{milestone.id}") + get api(resource_route) expect(response).to have_http_status(401) end it 'returns a 404 error if milestone id not found' do - get api("/projects/#{project.id}/milestones/1234", user) + get api("#{route}/1234", user) expect(response).to have_http_status(404) end end - describe 'POST /projects/:id/milestones' do - it 'creates a new project milestone' do - post api("/projects/#{project.id}/milestones", user), title: 'new milestone' + describe "POST #{route_definition}" do + it 'creates a new milestone' do + post api(route, user), title: 'new milestone' expect(response).to have_http_status(201) expect(json_response['title']).to eq('new milestone') expect(json_response['description']).to be_nil end - it 'creates a new project milestone with description and dates' do - post api("/projects/#{project.id}/milestones", user), + it 'creates a new milestone with description and dates' do + post api(route, user), title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02' expect(response).to have_http_status(201) @@ -152,20 +143,20 @@ describe API::Milestones do end it 'returns a 400 error if title is missing' do - post api("/projects/#{project.id}/milestones", user) + post api(route, user) expect(response).to have_http_status(400) end it 'returns a 400 error if params are invalid (duplicate title)' do - post api("/projects/#{project.id}/milestones", user), + post api(route, user), title: milestone.title, description: 'release', due_date: '2013-03-02' expect(response).to have_http_status(400) end - it 'creates a new project with reserved html characters' do - post api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2' + it 'creates a new milestone with reserved html characters' do + post api(route, user), title: 'foo & bar 1.1 -> 2.2' expect(response).to have_http_status(201) expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2') @@ -173,9 +164,9 @@ describe API::Milestones do end end - describe 'PUT /projects/:id/milestones/:milestone_id' do - it 'updates a project milestone' do - put api("/projects/#{project.id}/milestones/#{milestone.id}", user), + describe "PUT #{route_definition}/:milestone_id" do + it 'updates a milestone' do + put api(resource_route, user), title: 'updated title' expect(response).to have_http_status(200) @@ -185,23 +176,21 @@ describe API::Milestones do it 'removes a due date if nil is passed' do milestone.update!(due_date: "2016-08-05") - put api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil + put api(resource_route, user), due_date: nil expect(response).to have_http_status(200) expect(json_response['due_date']).to be_nil end it 'returns a 404 error if milestone id not found' do - put api("/projects/#{project.id}/milestones/1234", user), + put api("#{route}/1234", user), title: 'updated title' expect(response).to have_http_status(404) end - end - describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do - it 'updates a project milestone' do - put api("/projects/#{project.id}/milestones/#{milestone.id}", user), + it 'closes milestone' do + put api(resource_route, user), state_event: 'close' expect(response).to have_http_status(200) @@ -209,21 +198,14 @@ describe API::Milestones do end end - describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do - it 'creates an activity event when an milestone is closed' do - expect(Event).to receive(:create) + describe "GET #{route_definition}/:milestone_id/issues" do + let(:issues_route) { "#{route}/#{milestone.id}/issues" } - put api("/projects/#{project.id}/milestones/#{milestone.id}", user), - state_event: 'close' - end - end - - describe 'GET /projects/:id/milestones/:milestone_id/issues' do before do milestone.issues << create(:issue, project: project) end - it 'returns project issues for a particular milestone' do - get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + it 'returns issues for a particular milestone' do + get api(issues_route, user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -231,12 +213,12 @@ describe API::Milestones do expect(json_response.first['milestone']['title']).to eq(milestone.title) end - it 'returns project issues sorted by label priority' do + it 'returns issues sorted by label priority' do issue_1 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_3]) issue_2 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_1]) issue_3 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_2]) - get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + get api(issues_route, user) expect(json_response.first['id']).to eq(issue_2.id) expect(json_response.second['id']).to eq(issue_3.id) @@ -244,44 +226,58 @@ describe API::Milestones do end it 'matches V4 response schema for a list of issues' do - get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + get api(issues_route, user) expect(response).to have_http_status(200) expect(response).to match_response_schema('public_api/v4/issues') end it 'returns a 401 error if user not authenticated' do - get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") + get api(issues_route) expect(response).to have_http_status(401) end describe 'confidential issues' do - let(:public_project) { create(:empty_project, :public) } - let(:milestone) { create(:milestone, project: public_project) } - let(:issue) { create(:issue, project: public_project) } - let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } + let!(:public_project) { create(:empty_project, :public) } + let!(:context_group) { try(:group) } + let!(:milestone) do + context_group ? create(:milestone, group: context_group) : create(:milestone, project: public_project) + end + let!(:issue) { create(:issue, project: public_project) } + let!(:confidential_issue) { create(:issue, confidential: true, project: public_project) } + let!(:issues_route) do + if context_group + "#{route}/#{milestone.id}/issues" + else + "/projects/#{public_project.id}/milestones/#{milestone.id}/issues" + end + end before do + # Add public project to the group in context + setup_for_group if context_group + public_project.team << [user, :developer] milestone.issues << issue << confidential_issue end it 'returns confidential issues to team members' do - get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) + get api(issues_route, user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.size).to eq(2) + # 2 for projects, 3 for group(which has another project with an issue) + expect(json_response.size).to be_between(2, 3) expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) end it 'does not return confidential issues to team members with guest role' do member = create(:user) - project.team << [member, :guest] + public_project.team << [member, :guest] - get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) + get api(issues_route, member) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -291,7 +287,7 @@ describe API::Milestones do end it 'does not return confidential issues to regular users' do - get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user)) + get api(issues_route, create(:user)) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -304,30 +300,30 @@ describe API::Milestones do issue.labels << label_2 confidential_issue.labels << label_1 - get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) + get api(issues_route, user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.size).to eq(2) + # 2 for projects, 3 for group(which has another project with an issue) + expect(json_response.size).to be_between(2, 3) expect(json_response.first['id']).to eq(confidential_issue.id) expect(json_response.second['id']).to eq(issue.id) end end end - describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do - let(:merge_request) { create(:merge_request, source_project: project) } - let(:another_merge_request) { create(:merge_request, :simple, source_project: project) } + describe "GET #{route_definition}/:milestone_id/merge_requests" do + let(:merge_requests_route) { "#{route}/#{milestone.id}/merge_requests" } before do milestone.merge_requests << merge_request end - it 'returns project merge_requests for a particular milestone' do + it 'returns merge_requests for a particular milestone' do # eager-load another_merge_request another_merge_request - get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + get api(merge_requests_route, user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -336,12 +332,12 @@ describe API::Milestones do expect(json_response.first['milestone']['title']).to eq(milestone.title) end - it 'returns project merge_requests sorted by label priority' do + it 'returns merge_requests sorted by label priority' do merge_request_1 = create(:labeled_merge_request, source_branch: 'branch_1', source_project: project, milestone: milestone, labels: [label_2]) merge_request_2 = create(:labeled_merge_request, source_branch: 'branch_2', source_project: project, milestone: milestone, labels: [label_1]) merge_request_3 = create(:labeled_merge_request, source_branch: 'branch_3', source_project: project, milestone: milestone, labels: [label_3]) - get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + get api(merge_requests_route, user) expect(json_response.first['id']).to eq(merge_request_2.id) expect(json_response.second['id']).to eq(merge_request_1.id) @@ -349,20 +345,22 @@ describe API::Milestones do end it 'returns a 404 error if milestone id not found' do - get api("/projects/#{project.id}/milestones/1234/merge_requests", user) + not_found_route = "#{route}/1234/merge_requests" + + get api(not_found_route, user) expect(response).to have_http_status(404) end it 'returns a 404 if the user has no access to the milestone' do new_user = create :user - get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user) + get api(merge_requests_route, new_user) expect(response).to have_http_status(404) end it 'returns a 401 error if user not authenticated' do - get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests") + get api(merge_requests_route) expect(response).to have_http_status(401) end @@ -372,7 +370,7 @@ describe API::Milestones do another_merge_request.labels << label_1 merge_request.labels << label_2 - get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + get api(merge_requests_route, user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers From e4391c7190fcebd37e49db447b22b1081dca9741 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 21 Jul 2017 18:45:12 +0100 Subject: [PATCH 053/143] Backport changes from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2328 --- app/controllers/admin/application_settings_controller.rb | 4 ++-- app/controllers/projects/application_controller.rb | 1 + app/controllers/projects_controller.rb | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index c1bc4c0d675..4c0f7556894 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -76,11 +76,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.require(:application_setting).permit( - application_setting_params_ce + application_setting_params_attributes ) end - def application_setting_params_ce + def application_setting_params_attributes [ :admin_notification_email, :after_sign_out_path, diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 95de3a44641..221e01b415a 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -22,6 +22,7 @@ class Projects::ApplicationController < ApplicationController def project return @project if @project + return nil unless params[:project_id] || params[:id] path = File.join(params[:namespace_id], params[:project_id] || params[:id]) auth_proc = ->(project) { !project.pending_delete? } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c769693255c..2d7cbd4614e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -296,10 +296,10 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project) - .permit(project_params_ce) + .permit(project_params_attributes) end - def project_params_ce + def project_params_attributes [ :avatar, :build_allow_git_fetch, From fee65beb70bb9f995fe701a9deb0fabdc7a0e142 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 24 Jul 2017 09:20:22 +0100 Subject: [PATCH 054/143] Fixed custom logo sizing in new navigation header Closes #35439 --- app/assets/stylesheets/new_nav.scss | 5 +++++ changelogs/unreleased/new-navigation-custom-logo.yml | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 changelogs/unreleased/new-navigation-custom-logo.yml diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 9f3e278ebfc..360ffda8d71 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -21,6 +21,11 @@ header.navbar-gitlab-new { padding-right: 0; color: currentColor; + img { + height: 28px; + margin-right: 10px; + } + > a { display: flex; align-items: center; diff --git a/changelogs/unreleased/new-navigation-custom-logo.yml b/changelogs/unreleased/new-navigation-custom-logo.yml new file mode 100644 index 00000000000..22e6c5dc7e5 --- /dev/null +++ b/changelogs/unreleased/new-navigation-custom-logo.yml @@ -0,0 +1,4 @@ +--- +title: Fix sizing of custom header logo in new navigation +merge_request: +author: From a8b33d7b5db99f47000316a8dc167106214ca4f8 Mon Sep 17 00:00:00 2001 From: Emilien Mottet Date: Mon, 24 Jul 2017 17:04:54 +0200 Subject: [PATCH 055/143] fix conflict pluralized --- app/assets/javascripts/merge_conflicts/merge_conflict_store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index c4e379a4a0b..8be7314ded8 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -175,7 +175,7 @@ import Cookies from 'js-cookie'; getConflictsCountText() { const count = this.getConflictsCount(); - const text = count ? 'conflicts' : 'conflict'; + const text = count > 1 ? 'conflicts' : 'conflict'; return `${count} ${text}`; }, From 6039fa610c43fea950a96628ae26158c475d42b2 Mon Sep 17 00:00:00 2001 From: Chenjerai Katanda Date: Mon, 24 Jul 2017 16:20:49 +0000 Subject: [PATCH 056/143] Add instructions for enabling the `pg_trgm` extension in the production db. As a workaround to [a fault during HA setup](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2501). --- doc/administration/high_availability/database.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index da9687aa849..ca6d8d2de67 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -97,9 +97,12 @@ If you use a cloud-managed service, or provide your own PostgreSQL: Enter new password: Enter it again: ``` - -1. Enable the `pg_trgm` extension: +1. Exit from editing `template1` prompt by typing `\q` and Enter. +1. Enable the `pg_trgm` extension within the `gitlabhq_production` database: + ``` + gitlab-psql -d gitlabhq_production + CREATE EXTENSION pg_trgm; # Output: From 8bf89cb4aba188cd9abc41bb9eefb92458cfb75b Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Thu, 20 Jul 2017 22:44:48 +0200 Subject: [PATCH 057/143] Add author_id & assignee_id param to /issues API Allow issues filtering on `author_id` and `assignee_id`. --- app/finders/issuable_finder.rb | 1 + .../unreleased/tc-issue-api-assignee.yml | 4 +++ doc/api/issues.md | 13 ++++++++- lib/api/issues.rb | 5 ++++ spec/requests/api/issues_spec.rb | 27 +++++++++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/tc-issue-api-assignee.yml diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index fc63e30c8fb..6fe17a2b99d 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -11,6 +11,7 @@ # group_id: integer # project_id: integer # milestone_title: string +# author_id: integer # assignee_id: integer # search: string # label_name: string diff --git a/changelogs/unreleased/tc-issue-api-assignee.yml b/changelogs/unreleased/tc-issue-api-assignee.yml new file mode 100644 index 00000000000..8d6360d5baf --- /dev/null +++ b/changelogs/unreleased/tc-issue-api-assignee.yml @@ -0,0 +1,4 @@ +--- +title: Add author_id & assignee_id param to /issues API +merge_request: 13004 +author: diff --git a/doc/api/issues.md b/doc/api/issues.md index a00a63bad4b..82c19c8c40a 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -26,7 +26,8 @@ GET /issues?labels=foo,bar&state=opened GET /issues?milestone=1.0.0 GET /issues?milestone=1.0.0&state=opened GET /issues?iids[]=42&iids[]=43 -GET /issues?search=issue+title+or+description +GET /issues?author_id=5 +GET /issues?assignee_id=5 ``` | Attribute | Type | Required | Description | @@ -34,6 +35,8 @@ GET /issues?search=issue+title+or+description | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `milestone` | string | no | The milestone title | +| `author_id` | integer | no | Returns issues created by the given user `id` (not limited to issues created by the authenticated user) | +| `assignee_id` | integer | no | Returns issues assigned to the given user `id` (not limited to issues created by the authenticated user) | | `iids` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | @@ -117,6 +120,8 @@ GET /groups/:id/issues?milestone=1.0.0 GET /groups/:id/issues?milestone=1.0.0&state=opened GET /groups/:id/issues?iids[]=42&iids[]=43 GET /groups/:id/issues?search=issue+title+or+description +GET /groups/:id/issues?author_id=5 +GET /groups/:id/issues?assignee_id=5 ``` | Attribute | Type | Required | Description | @@ -126,6 +131,8 @@ GET /groups/:id/issues?search=issue+title+or+description | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `iids` | Array[integer] | no | Return only the issues having the given `iid` | | `milestone` | string | no | The milestone title | +| `author_id` | integer | no | Returns issues created by the given user `id` (not limited to issues created by the authenticated user) | +| `assignee_id` | integer | no | Returns issues assigned to the given user `id` (not limited to issues created by the authenticated user) | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search group issues against their `title` and `description` | @@ -209,6 +216,8 @@ GET /projects/:id/issues?milestone=1.0.0 GET /projects/:id/issues?milestone=1.0.0&state=opened GET /projects/:id/issues?iids[]=42&iids[]=43 GET /projects/:id/issues?search=issue+title+or+description +GET /projects/:id/issues?author_id=5 +GET /projects/:id/issues?assignee_id=5 ``` | Attribute | Type | Required | Description | @@ -218,6 +227,8 @@ GET /projects/:id/issues?search=issue+title+or+description | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `milestone` | string | no | The milestone title | +| `author_id` | integer | no | Returns issues created by the given user `id` (not limited to issues created by the authenticated user) | +| `assignee_id` | integer | no | Returns issues assigned to the given user `id` (not limited to issues created by the authenticated user) | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search project issues against their `title` and `description` | diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 14b26f28ebf..621539afeaf 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -8,6 +8,9 @@ module API def find_issues(args = {}) args = params.merge(args) + # Do not scope to "authored" when author or assignee id is given + args.delete(:scope) if args[:author_id] || args[:assignee_id] + args.delete(:id) args[:milestone_title] = args.delete(:milestone) args[:label_name] = args.delete(:labels) @@ -29,6 +32,8 @@ module API optional :search, type: String, desc: 'Search issues for text present in the title or description' optional :created_after, type: DateTime, desc: 'Return issues created after the specified time' optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' + optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' + optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID' use :pagination end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9837fedb522..dac88ce0b07 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -105,6 +105,33 @@ describe API::Issues do expect(json_response.second['id']).to eq(closed_issue.id) end + it 'returns issues authored by the given author id' do + issue2 = create(:issue, author: user2, project: project) + + get api('/issues', user), author_id: user2.id + + expect_paginated_array_response(size: 1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues assigned to the given assignee id' do + issue2 = create(:issue, assignees: [user2], project: project) + + get api('/issues', user), assignee_id: user2.id + + expect_paginated_array_response(size: 1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues authored by the given author id and assigned to the given assignee id' do + issue2 = create(:issue, author: user2, assignees: [user2], project: project) + + get api('/issues', user), author_id: user2.id, assignee_id: user2.id + + expect_paginated_array_response(size: 1) + expect(first_issue['id']).to eq(issue2.id) + end + it 'returns issues matching given search string for title' do get api("/issues", user), search: issue.title From d8798c907dfb960856423422a91eb1e6dc8db090 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Mon, 24 Jul 2017 22:41:33 +0200 Subject: [PATCH 058/143] Allow query param scope for /issues API endpoint --- doc/api/issues.md | 15 +++++++++------ lib/api/issues.rb | 9 +++++---- spec/requests/api/issues_spec.rb | 16 ++++++++++++---- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/doc/api/issues.md b/doc/api/issues.md index 82c19c8c40a..466fea651af 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -35,8 +35,9 @@ GET /issues?assignee_id=5 | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `milestone` | string | no | The milestone title | -| `author_id` | integer | no | Returns issues created by the given user `id` (not limited to issues created by the authenticated user) | -| `assignee_id` | integer | no | Returns issues assigned to the given user `id` (not limited to issues created by the authenticated user) | +| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` | +| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id` | | `iids` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | @@ -131,8 +132,9 @@ GET /groups/:id/issues?assignee_id=5 | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `iids` | Array[integer] | no | Return only the issues having the given `iid` | | `milestone` | string | no | The milestone title | -| `author_id` | integer | no | Returns issues created by the given user `id` (not limited to issues created by the authenticated user) | -| `assignee_id` | integer | no | Returns issues assigned to the given user `id` (not limited to issues created by the authenticated user) | +| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` | +| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id` | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search group issues against their `title` and `description` | @@ -227,8 +229,9 @@ GET /projects/:id/issues?assignee_id=5 | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `milestone` | string | no | The milestone title | -| `author_id` | integer | no | Returns issues created by the given user `id` (not limited to issues created by the authenticated user) | -| `assignee_id` | integer | no | Returns issues assigned to the given user `id` (not limited to issues created by the authenticated user) | +| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` | +| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id` | | `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search project issues against their `title` and `description` | diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 621539afeaf..8f8d622fd34 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -8,9 +8,6 @@ module API def find_issues(args = {}) args = params.merge(args) - # Do not scope to "authored" when author or assignee id is given - args.delete(:scope) if args[:author_id] || args[:assignee_id] - args.delete(:id) args[:milestone_title] = args.delete(:milestone) args[:label_name] = args.delete(:labels) @@ -34,6 +31,8 @@ module API optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID' + optional :scope, type: String, values: %w[created-by-me assigned-to-me all], + desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`' use :pagination end @@ -60,9 +59,11 @@ module API optional :state, type: String, values: %w[opened closed all], default: 'all', desc: 'Return opened, closed, or all issues' use :issues_params + optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me', + desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`' end get do - issues = find_issues(scope: 'authored') + issues = find_issues present paginate(issues), with: Entities::IssueBasic, current_user: current_user end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index dac88ce0b07..c8f3267907a 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -71,7 +71,6 @@ describe API::Issues do expect(response).to have_http_status(401) end end - context "when authenticated" do let(:first_issue) { json_response.first } @@ -105,10 +104,19 @@ describe API::Issues do expect(json_response.second['id']).to eq(closed_issue.id) end + it 'returns issues assigned to me' do + issue2 = create(:issue, assignees: [user2], project: project) + + get api('/issues', user2), scope: 'assigned-to-me' + + expect_paginated_array_response(size: 1) + expect(first_issue['id']).to eq(issue2.id) + end + it 'returns issues authored by the given author id' do issue2 = create(:issue, author: user2, project: project) - get api('/issues', user), author_id: user2.id + get api('/issues', user), author_id: user2.id, scope: 'all' expect_paginated_array_response(size: 1) expect(first_issue['id']).to eq(issue2.id) @@ -117,7 +125,7 @@ describe API::Issues do it 'returns issues assigned to the given assignee id' do issue2 = create(:issue, assignees: [user2], project: project) - get api('/issues', user), assignee_id: user2.id + get api('/issues', user), assignee_id: user2.id, scope: 'all' expect_paginated_array_response(size: 1) expect(first_issue['id']).to eq(issue2.id) @@ -126,7 +134,7 @@ describe API::Issues do it 'returns issues authored by the given author id and assigned to the given assignee id' do issue2 = create(:issue, author: user2, assignees: [user2], project: project) - get api('/issues', user), author_id: user2.id, assignee_id: user2.id + get api('/issues', user), author_id: user2.id, assignee_id: user2.id, scope: 'all' expect_paginated_array_response(size: 1) expect(first_issue['id']).to eq(issue2.id) From 837e3e7c280897ba166704203775f7ff71e378d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Tue, 25 Jul 2017 14:25:59 +0800 Subject: [PATCH 059/143] synchronize ukrainian translation in zanata --- locale/uk/gitlab.po | 80 +++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po index 56498f3c901..b81f566309c 100644 --- a/locale/uk/gitlab.po +++ b/locale/uk/gitlab.po @@ -9,8 +9,8 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language-Team: Ukrainian (https://translate.zanata.org/project/view/GitLab)\n" -"PO-Revision-Date: 2017-07-14 01:22-0400\n" -"Last-Translator: Huang Tao \n" +"PO-Revision-Date: 2017-07-24 06:16-0400\n" +"Last-Translator: Андрей Витюк \n" "Language: uk\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " @@ -57,7 +57,7 @@ msgid "Add Changelog" msgstr "Додати список змін (Changelog)" msgid "Add Contribution guide" -msgstr "Додати керівництво для контрибуторів" +msgstr "Додати керівництво для контриб’юторів" msgid "Add License" msgstr "Додати ліцензію" @@ -209,7 +209,9 @@ msgstr[1] "Комміта" msgstr[2] "Коммітів" msgid "Commit duration in minutes for last 30 commits" -msgstr "Комміт тривалість у хвилинах за останні 30 коммітів" +msgstr "" +"Тривалість коммітів протягом декількох хвилин на протязі 30 останніх " +"коммітів" msgid "Commit message" msgstr "Комміт повідомлення" @@ -236,10 +238,10 @@ msgid "Compare" msgstr "Порівняти" msgid "Contribution guide" -msgstr "Керівництво контрибуторів" +msgstr "Керівництво контриб’юторів" msgid "Contributors" -msgstr "Контрибутори" +msgstr "Контриб’ютори" msgid "Copy URL to clipboard" msgstr "Скопіювати URL в буфер обміну" @@ -352,16 +354,16 @@ msgid "Download" msgstr "Завантажити" msgid "Download tar" -msgstr "Завантажити в форматі tar" +msgstr "Завантажити tar" msgid "Download tar.bz2" -msgstr "Завантажити в форматі tar.bz2" +msgstr "Завантажити tar.bz2" msgid "Download tar.gz" -msgstr "Завантажити в форматі tar.gz" +msgstr "Завантажити tar.gz" msgid "Download zip" -msgstr "Завантажити в форматі zip" +msgstr "Завантажити zip" msgid "DownloadArtifacts|Download" msgstr "Завантажити" @@ -397,7 +399,7 @@ msgid "Failed to remove the pipeline schedule" msgstr "Не вдалося видалити розклад Конвеєра" msgid "Files" -msgstr "Файли" +msgstr "Файлів" msgid "Filter by commit message" msgstr "Фільтрувати повідомлення коммітів" @@ -436,7 +438,7 @@ msgid "GoToYourFork|Fork" msgstr "Форк" msgid "Home" -msgstr "Початок" +msgstr "Головна" msgid "Housekeeping successfully started" msgstr "Очищення успішно розпочато" @@ -451,13 +453,13 @@ msgid "Introducing Cycle Analytics" msgstr "Представляємо аналітику циклу" msgid "Jobs for last month" -msgstr "Завдання за останній місяць" +msgstr "Кількість завдань за останній місяць" msgid "Jobs for last week" -msgstr "Завдання за останній тиждень" +msgstr "Кількість завдань за останній тиждень" msgid "Jobs for last year" -msgstr "Завдання за останній рік" +msgstr "Кількість завдань за останній рік" msgid "LFSStatus|Disabled" msgstr "Вимкнено" @@ -508,7 +510,7 @@ msgid "New Issue" msgid_plural "New Issues" msgstr[0] "Нова проблема" msgstr[1] "Нові проблеми" -msgstr[2] "Новах проблем" +msgstr[2] "Нових проблем" msgid "New Pipeline Schedule" msgstr "Новий розклад Конвеєра" @@ -757,7 +759,7 @@ msgid "ProjectLifecycle|Stage" msgstr "Етап" msgid "ProjectNetworkGraph|Graph" -msgstr "Графік" +msgstr "Історія" msgid "Read more" msgstr "Докладніше" @@ -852,7 +854,7 @@ msgid "Source code" msgstr "Код" msgid "StarProject|Star" -msgstr "Старт" +msgstr "Підписатися" msgid "Start a %{new_merge_request} with these changes" msgstr "Почати %{new_merge_request} з цих змін" @@ -988,19 +990,19 @@ msgid "Timeago|%s minutes remaining" msgstr "%s хвилини залишитися" msgid "Timeago|%s months ago" -msgstr "%s місяців тому" +msgstr "%s місяці(в) тому" msgid "Timeago|%s months remaining" -msgstr "%s місяці, що залишилися" +msgstr "%s місяці(в), що залишилися" msgid "Timeago|%s seconds remaining" msgstr "%s секунд, що залишаються" msgid "Timeago|%s weeks ago" -msgstr "%s тижнів тому" +msgstr "%s тижні(в) тому" msgid "Timeago|%s weeks remaining" -msgstr "%s тижнів залишилися" +msgstr "%s тижні(в) залишилися" msgid "Timeago|%s years ago" msgstr "%s років тому" @@ -1030,7 +1032,7 @@ msgid "Timeago|Past due" msgstr "Прострочені" msgid "Timeago|a day ago" -msgstr "годин тому" +msgstr "День тому" msgid "Timeago|a month ago" msgstr "місяць тому" @@ -1054,28 +1056,28 @@ msgid "Timeago|about an hour ago" msgstr "Близько години тому" msgid "Timeago|in %s days" -msgstr "через %s днїв" +msgstr "через %s дні(в)" msgid "Timeago|in %s hours" -msgstr "через %s години" +msgstr "через %s годин(и)" msgid "Timeago|in %s minutes" -msgstr "через %s хвилини" +msgstr "через %s хвилин(и)" msgid "Timeago|in %s months" -msgstr "через %s місяців" +msgstr "через %s місяці(в)" msgid "Timeago|in %s seconds" -msgstr "через %s секунд" +msgstr "через %s секунд(и)" msgid "Timeago|in %s weeks" -msgstr "через %s тижні" +msgstr "через %s тижні(в)" msgid "Timeago|in %s years" -msgstr "через %s років" +msgstr "через %s роки(ів)" msgid "Timeago|in 1 day" -msgstr "через день" +msgstr "через 1 день" msgid "Timeago|in 1 hour" msgstr "через годину" @@ -1093,22 +1095,22 @@ msgid "Timeago|in 1 year" msgstr "через рік" msgid "Timeago|less than a minute ago" -msgstr "менш хвилини тому" +msgstr "менше хвилини тому" msgid "Time|hr" msgid_plural "Time|hrs" -msgstr[0] "Година" -msgstr[1] "Годині" -msgstr[2] "Годин" +msgstr[0] "година" +msgstr[1] "години" +msgstr[2] "годин" msgid "Time|min" msgid_plural "Time|mins" msgstr[0] "хвилина" -msgstr[1] "хвилині" +msgstr[1] "хвилини" msgstr[2] "хвилин" msgid "Time|s" -msgstr "секунда" +msgstr "секунд(а)" msgid "Total Time" msgstr "Загальний час" @@ -1117,7 +1119,7 @@ msgid "Total test time for all commits/merges" msgstr "Загальний час, щоб перевірити всі фіксації/злиття" msgid "Unstar" -msgstr "Зняти позначку" +msgstr "Відписатись" msgid "Upload New File" msgstr "Завантажити новий файл" @@ -1129,7 +1131,7 @@ msgid "UploadLink|click to upload" msgstr "Натисніть, щоб завантажити" msgid "Use your global notification setting" -msgstr "Використовуються глобальний налаштування повідомлень" +msgstr "Використовуються глобальні налаштування повідомлень" msgid "View open merge request" msgstr "Перегляд відкритих запитів на злиття" From 2f0a4243d5e7d848172a8adfa72084eb4d07c60b Mon Sep 17 00:00:00 2001 From: Frank Groeneveld Date: Tue, 25 Jul 2017 08:29:04 +0200 Subject: [PATCH 060/143] Upgrade re2 to support seperate CXX and CC passing --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 5758b1b554e..8b32a60818a 100644 --- a/Gemfile +++ b/Gemfile @@ -164,7 +164,7 @@ gem 'rainbow', '~> 2.2' gem 'settingslogic', '~> 2.0.9' # Linear-time regex library for untrusted regular expressions -gem 're2', '~> 1.1.0' +gem 're2', '~> 1.1.1' # Misc diff --git a/Gemfile.lock b/Gemfile.lock index 6ffff0d8735..a64805ad6bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -656,7 +656,7 @@ GEM debugger-ruby_core_source (~> 1.3) rdoc (4.2.2) json (~> 1.4) - re2 (1.1.0) + re2 (1.1.1) recaptcha (3.0.0) json recursive-open-struct (1.0.0) @@ -1055,7 +1055,7 @@ DEPENDENCIES raindrops (~> 0.18) rblineprof (~> 0.3.6) rdoc (~> 4.2) - re2 (~> 1.1.0) + re2 (~> 1.1.1) recaptcha (~> 3.0) redcarpet (~> 3.4) redis (~> 3.2) From 2b0a85c100423adf648c99ae3f528c46e5d474c7 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 18 Jul 2017 09:21:09 +0200 Subject: [PATCH 061/143] Adjust `PathRegex` to validate files in the `public` directory And reports when too many words are rejected. --- spec/lib/gitlab/path_regex_spec.rb | 46 +++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 1eea710c80b..37c67db8217 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -36,9 +36,10 @@ describe Gitlab::PathRegex, lib: true do described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first) end - def failure_message(missing_words, constant_name, migration_helper) + def failure_message(constant_name, migration_helper, missing_words:, additional_words: []) missing_words = Array(missing_words) - <<-MSG + additional_words = Array(additional_words) + message = <<-MSG Found new routes that could cause conflicts with existing namespaced routes for groups or projects. @@ -52,6 +53,18 @@ describe Gitlab::PathRegex, lib: true do Make sure to make a note of the renamed records in the release blog post. MSG + + if additional_words.any? + additional_message = <<-ADDITIONAL + Why are <#{additional_words.join(', ')}> in `#{constant_name}`? + If they are really required, update these specs to reflect that. + + ADDITIONAL + + message = [message, additional_message].join + end + + message end let(:all_routes) do @@ -68,9 +81,26 @@ describe Gitlab::PathRegex, lib: true do let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } let(:top_level_words) do - routes_not_starting_in_wildcard.map do |route| + words = routes_not_starting_in_wildcard.map do |route| route.split('/')[1] end.compact.uniq + + words += files_in_public + words + additional_top_level_words + end + + let(:additional_top_level_words) do + # Required to keep the uploads safe, remove after + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12917 gets merged + ['system'] + end + + let(:files_in_public) do + git = Gitlab.config.git.bin_path + `cd #{Rails.root} && #{git} ls-files public` + .split("\n") + .map { |entry| entry.gsub('public/', '') } + .uniq end # All routes that start with a namespaced path, that have 1 or more @@ -122,11 +152,13 @@ describe Gitlab::PathRegex, lib: true do it 'includes all the top level namespaces' do failure_block = lambda do missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES - failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths') + additional_words = described_class::TOP_LEVEL_ROUTES - top_level_words + failure_message('TOP_LEVEL_ROUTES', 'rename_root_paths', + missing_words: missing_words, additional_words: additional_words) end expect(described_class::TOP_LEVEL_ROUTES) - .to include(*top_level_words), failure_block + .to contain_exactly(*top_level_words), failure_block end end @@ -134,7 +166,7 @@ describe Gitlab::PathRegex, lib: true do it "don't contain a second wildcard" do failure_block = lambda do missing_words = paths_after_group_id - described_class::GROUP_ROUTES - failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths') + failure_message('GROUP_ROUTES', 'rename_child_paths', missing_words: missing_words) end expect(described_class::GROUP_ROUTES) @@ -147,7 +179,7 @@ describe Gitlab::PathRegex, lib: true do aggregate_failures do all_wildcard_paths.each do |path| expect(wildcards_include?(path)) - .to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths') + .to be(true), failure_message('PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths', missing_words: path) end end end From 1dcf799c76c5f2218ed7b1997389cd1e4ac81a17 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 18 Jul 2017 09:25:27 +0200 Subject: [PATCH 062/143] Remove a bunch of reserved top level routes These don't seem to be used anywhere, so can be removed. --- lib/gitlab/path_regex.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 60a32d5d5ea..c8b1b762940 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -16,7 +16,6 @@ module Gitlab .well-known abuse_reports admin - all api assets autocomplete @@ -27,29 +26,20 @@ module Gitlab groups health_check help - hooks import invites - issues jwt koding - member - merge_requests - new - notes notification_settings oauth profile projects public - repository robots.txt s search sent_notifications - services snippets - teams u unicorn_test unsubscribes From bf114b31114e860e746f248661addcdde0133077 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 18 Jul 2017 09:37:38 +0200 Subject: [PATCH 063/143] Add contents of `public` as forbidden top-level routes --- lib/gitlab/path_regex.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index c8b1b762940..894bd5efae5 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -14,14 +14,23 @@ module Gitlab TOP_LEVEL_ROUTES = %w[ - .well-known + 404.html + 422.html + 500.html + 502.html + 503.html abuse_reports admin api + apple-touch-icon-precomposed.png + apple-touch-icon.png assets autocomplete ci dashboard + deploy.html explore + favicon.ico files groups health_check @@ -39,6 +48,7 @@ module Gitlab s search sent_notifications + slash-command-logo.png snippets u unicorn_test From d22fe96b58b104830f99fa77cba2d4fe7d7aaaff Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 18 Jul 2017 15:28:20 +0200 Subject: [PATCH 064/143] Take ee words into account We need to reserve these words in EE to support the upgrade path from CE to EE. --- .../unreleased/bvl-free-unused-names.yml | 5 +++ spec/lib/gitlab/path_regex_spec.rb | 39 ++++++++++++------- 2 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 changelogs/unreleased/bvl-free-unused-names.yml diff --git a/changelogs/unreleased/bvl-free-unused-names.yml b/changelogs/unreleased/bvl-free-unused-names.yml new file mode 100644 index 00000000000..53acb95e5bb --- /dev/null +++ b/changelogs/unreleased/bvl-free-unused-names.yml @@ -0,0 +1,5 @@ +--- +title: Free up some top level words, reject top level groups named like files in the + public folder +merge_request: 12932 +author: diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 37c67db8217..c38bbb64fc3 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -36,10 +36,12 @@ describe Gitlab::PathRegex, lib: true do described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first) end - def failure_message(constant_name, migration_helper, missing_words:, additional_words: []) + def failure_message(constant_name, migration_helper, missing_words: [], additional_words: []) missing_words = Array(missing_words) additional_words = Array(additional_words) - message = <<-MSG + message = "" + if missing_words.any? + message += <<-MISSING Found new routes that could cause conflicts with existing namespaced routes for groups or projects. @@ -52,16 +54,15 @@ describe Gitlab::PathRegex, lib: true do Make sure to make a note of the renamed records in the release blog post. - MSG + MISSING + end if additional_words.any? - additional_message = <<-ADDITIONAL + message += <<-ADDITIONAL Why are <#{additional_words.join(', ')}> in `#{constant_name}`? If they are really required, update these specs to reflect that. ADDITIONAL - - message = [message, additional_message].join end message @@ -85,14 +86,11 @@ describe Gitlab::PathRegex, lib: true do route.split('/')[1] end.compact.uniq - words += files_in_public - words + additional_top_level_words + words + ee_top_level_words + files_in_public end - let(:additional_top_level_words) do - # Required to keep the uploads safe, remove after - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12917 gets merged - ['system'] + let(:ee_top_level_words) do + ['unsubscribes'] end let(:files_in_public) do @@ -145,7 +143,16 @@ describe Gitlab::PathRegex, lib: true do let(:paths_after_group_id) do group_routes.map do |route| route.gsub(STARTING_WITH_GROUP, '').split('/').first - end.uniq + end.uniq + ee_paths_after_group_id + end + + let(:ee_paths_after_group_id) do + %w(analytics + ldap + ldap_group_links + notification_setting + audit_events + pipeline_quota hooks) end describe 'TOP_LEVEL_ROUTES' do @@ -166,11 +173,13 @@ describe Gitlab::PathRegex, lib: true do it "don't contain a second wildcard" do failure_block = lambda do missing_words = paths_after_group_id - described_class::GROUP_ROUTES - failure_message('GROUP_ROUTES', 'rename_child_paths', missing_words: missing_words) + additional_words = described_class::GROUP_ROUTES - paths_after_group_id + failure_message('GROUP_ROUTES', 'rename_child_paths', + missing_words: missing_words, additional_words: additional_words) end expect(described_class::GROUP_ROUTES) - .to include(*paths_after_group_id), failure_block + .to contain_exactly(*paths_after_group_id), failure_block end end From 02987e17c7af928fb85f80d1039eb938c366d8d3 Mon Sep 17 00:00:00 2001 From: Balasankar C Date: Tue, 25 Jul 2017 08:19:34 +0000 Subject: [PATCH 065/143] Update docs on using external registry with gitlab --- doc/administration/container_registry.md | 29 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index afafb6bf1f5..8cb0e5b1562 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -465,23 +465,42 @@ on how to achieve that. ## Disable Container Registry but use GitLab as an auth endpoint -You can disable the embedded Container Registry to use an external one, but -still use GitLab as an auth endpoint. - **Omnibus GitLab** + +You can use GitLab as an auth endpoint and use a non-bundled Container Registry. + 1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations: ```ruby - registry['enable'] = false gitlab_rails['registry_enabled'] = true gitlab_rails['registry_host'] = "registry.gitlab.example.com" gitlab_rails['registry_port'] = "5005" gitlab_rails['registry_api_url'] = "http://localhost:5000" - gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key" gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry" gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer" ``` +1. A certificate keypair is required for GitLab and the Container Registry to + communicate securely. By default omnibus-gitlab will generate one keypair, + which is saved to `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key`. + When using an non-bundled Container Registry, you will need to supply a + custom certificate key. To do that, add the following to + `/etc/gitlab/gitlab.rb` + + ```ruby + gitlab_rails['registry_key_path'] = "/custom/path/to/registry-key.key" + # registry['internal_key'] should contain the contents of the custom key + # file. Line breaks in the key file should be marked using `\n` character + # Example: + registry['internal_key'] = "---BEGIN RSA PRIVATE KEY---\nMIIEpQIBAA\n" + ``` + + **Note:** The file specified at `registry_key_path` gets populated with the + content specified by `internal_key`, each time reconfigure is executed. If + no file is specified, omnibus-gitlab will default it to + `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` and will populate + it. + 1. Save the file and [reconfigure GitLab][] for the changes to take effect. **Installations from source** From 25e44edc30b5ca61267487248db9330da3e48a6c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 25 Jul 2017 16:44:02 +0800 Subject: [PATCH 066/143] Allow admin to read_users_list even if it's restricted --- app/policies/global_policy.rb | 2 +- .../35478-allow-admin-to-read-user-list.yml | 4 ++++ spec/policies/global_policy_spec.rb | 20 +++++++++++++++++++ spec/requests/api/users_spec.rb | 19 +++++++++++------- 4 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/35478-allow-admin-to-read-user-list.yml diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 55eefa76d3f..1c91425f589 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -44,7 +44,7 @@ class GlobalPolicy < BasePolicy prevent :log_in end - rule { ~restricted_public_level }.policy do + rule { admin | ~restricted_public_level }.policy do enable :read_users_list end end diff --git a/changelogs/unreleased/35478-allow-admin-to-read-user-list.yml b/changelogs/unreleased/35478-allow-admin-to-read-user-list.yml new file mode 100644 index 00000000000..da4b730f0ca --- /dev/null +++ b/changelogs/unreleased/35478-allow-admin-to-read-user-list.yml @@ -0,0 +1,4 @@ +--- +title: Allow admin to read_users_list even if it's restricted +merge_request: 13066 +author: diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index bb0fa0c0e9c..c3e2b603c4b 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -30,5 +30,25 @@ describe GlobalPolicy, models: true do it { is_expected.to be_allowed(:read_users_list) } end end + + context "for an admin" do + let(:current_user) { create(:admin) } + + context "when the public level is restricted" do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it { is_expected.to be_allowed(:read_users_list) } + end + + context "when the public level is not restricted" do + before do + stub_application_setting(restricted_visibility_levels: []) + end + + it { is_expected.to be_allowed(:read_users_list) } + end + end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 877bde3b9a6..66b165b438b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -55,17 +55,22 @@ describe API::Users do context "when public level is restricted" do before do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) - allow_any_instance_of(API::Helpers).to receive(:authenticate!).and_return(true) end - it "renders 403" do - get api("/users") - expect(response).to have_http_status(403) + context 'when authenticate as a regular user' do + it "renders 403" do + get api("/users", user) + + expect(response).to have_gitlab_http_status(403) + end end - it "renders 404" do - get api("/users/#{user.id}") - expect(response).to have_http_status(404) + context 'when authenticate as an admin' do + it "renders 200" do + get api("/users", admin) + + expect(response).to have_gitlab_http_status(200) + end end end From e13d75c38a09fca98dfbb52ef94119770b7a445a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Sun, 2 Jul 2017 17:02:59 +0200 Subject: [PATCH 067/143] Explicitly define inverse of acces_level relations --- app/models/concerns/protected_ref.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index fc6b840f7a8..5dd43c36222 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -17,7 +17,13 @@ module ProtectedRef class_methods do def protected_ref_access_levels(*types) types.each do |type| - has_many :"#{type}_access_levels", dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + # We need to set `inverse_of` to make sure the `belongs_to`-object is set + # when creating children using `accepts_nested_attributes_for`. + # + # If we don't `protected_branch` or `protected_tag` would be empty and + # `project` cannot be delegated to it, which in turn would cause validations + # to fail. + has_many :"#{type}_access_levels", dependent: :destroy, inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." } From 33dc5171e5885bbc1de1db7b9be58453edfa9453 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Tue, 25 Jul 2017 09:35:45 +0000 Subject: [PATCH 068/143] Resolve "More RESTful API: include resource URLs in responses" --- Gemfile | 1 + Gemfile.lock | 5 + ...ources-uris-using-grape-source-helpers.yml | 4 + .../initializers/grape_route_helpers_fix.rb | 35 +++++++ config/routes/api.rb | 2 +- doc/api/issues.md | 40 +++++++- doc/api/projects.md | 91 ++++++++++++++++++- lib/api/api.rb | 1 + lib/api/entities.rb | 52 +++++++++++ lib/api/helpers/related_resources_helpers.rb | 28 ++++++ lib/api/issues.rb | 2 +- lib/api/v3/entities.rb | 31 ++++++- spec/requests/api/issues_spec.rb | 13 +++ spec/requests/api/projects_spec.rb | 32 +++++++ 14 files changed, 324 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml create mode 100644 config/initializers/grape_route_helpers_fix.rb create mode 100644 lib/api/helpers/related_resources_helpers.rb diff --git a/Gemfile b/Gemfile index 210ac78fac3..d24d10e7496 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ gem 'mysql2', '~> 0.4.5', group: :mysql gem 'pg', '~> 0.18.2', group: :postgres gem 'rugged', '~> 0.25.1.1' +gem 'grape-route-helpers', '~> 2.0.0' gem 'faraday', '~> 0.12' diff --git a/Gemfile.lock b/Gemfile.lock index f6c1636dfaf..1f3d6d2d618 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -345,6 +345,10 @@ GEM grape-entity (0.6.0) activesupport multi_json (>= 1.3.2) + grape-route-helpers (2.0.0) + activesupport + grape (~> 0.16, >= 0.16.0) + rake grpc (1.4.0) google-protobuf (~> 3.1) googleauth (~> 0.5.1) @@ -981,6 +985,7 @@ DEPENDENCIES google-api-client (~> 0.8.6) grape (~> 0.19.2) grape-entity (~> 0.6.0) + grape-route-helpers (~> 2.0.0) haml_lint (~> 0.21.0) hamlit (~> 2.6.1) hashie-forbidden_attributes diff --git a/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml b/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml new file mode 100644 index 00000000000..837a34bd067 --- /dev/null +++ b/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml @@ -0,0 +1,4 @@ +--- +title: Declare related resources into V4 API entities +merge_request: +author: diff --git a/config/initializers/grape_route_helpers_fix.rb b/config/initializers/grape_route_helpers_fix.rb new file mode 100644 index 00000000000..d3cf9e453d0 --- /dev/null +++ b/config/initializers/grape_route_helpers_fix.rb @@ -0,0 +1,35 @@ +if defined?(GrapeRouteHelpers) + module GrapeRouteHelpers + class DecoratedRoute + # GrapeRouteHelpers gem tries to parse the versions + # from a string, not supporting Grape `version` array definition. + # + # Without the following fix, we get this on route helpers generation: + # + # => undefined method `scan' for ["v3", "v4"] + # + # 2.0.0 implementation of this method: + # + # ``` + # def route_versions + # version_pattern = /[^\[",\]\s]+/ + # if route_version + # route_version.scan(version_pattern) + # else + # [nil] + # end + # end + # ``` + def route_versions + return [nil] if route_version.nil? || route_version.empty? + + if route_version.is_a?(String) + version_pattern = /[^\[",\]\s]+/ + route_version.scan(version_pattern) + else + route_version + end + end + end + end +end diff --git a/config/routes/api.rb b/config/routes/api.rb index 69c8efc151c..ce7a7c88900 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -1,2 +1,2 @@ API::API.logger Rails.logger -mount API::API => '/api' +mount API::API => '/' diff --git a/doc/api/issues.md b/doc/api/issues.md index a00a63bad4b..0e391c75cd3 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -356,7 +356,13 @@ Example response: "user_notes_count": 1, "due_date": null, "web_url": "http://example.com/example/example/issues/1", - "confidential": false + "confidential": false, + "_links": { + "self": "http://example.com/api/v4/projects/1/issues/2", + "notes": "http://example.com/api/v4/projects/1/issues/2/notes", + "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji", + "project": "http://example.com/api/v4/projects/1" + } } ``` @@ -418,7 +424,13 @@ Example response: "user_notes_count": 0, "due_date": null, "web_url": "http://example.com/example/example/issues/14", - "confidential": false + "confidential": false, + "_links": { + "self": "http://example.com/api/v4/projects/1/issues/2", + "notes": "http://example.com/api/v4/projects/1/issues/2/notes", + "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji", + "project": "http://example.com/api/v4/projects/1" + } } ``` @@ -481,7 +493,13 @@ Example response: "user_notes_count": 0, "due_date": "2016-07-22", "web_url": "http://example.com/example/example/issues/15", - "confidential": false + "confidential": false, + "_links": { + "self": "http://example.com/api/v4/projects/1/issues/2", + "notes": "http://example.com/api/v4/projects/1/issues/2/notes", + "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji", + "project": "http://example.com/api/v4/projects/1" + } } ``` @@ -567,7 +585,13 @@ Example response: }, "due_date": null, "web_url": "http://example.com/example/example/issues/11", - "confidential": false + "confidential": false, + "_links": { + "self": "http://example.com/api/v4/projects/1/issues/2", + "notes": "http://example.com/api/v4/projects/1/issues/2/notes", + "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji", + "project": "http://example.com/api/v4/projects/1" + } } ``` @@ -632,7 +656,13 @@ Example response: }, "due_date": null, "web_url": "http://example.com/example/example/issues/11", - "confidential": false + "confidential": false, + "_links": { + "self": "http://example.com/api/v4/projects/1/issues/2", + "notes": "http://example.com/api/v4/projects/1/issues/2/notes", + "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji", + "project": "http://example.com/api/v4/projects/1" + } } ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 61ae89a64c0..d3f8e509612 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -99,7 +99,16 @@ Parameters: "repository_size": 1038090, "lfs_objects_size": 0, "job_artifacts_size": 0 - } + }, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" + }, }, { "id": 6, @@ -168,6 +177,15 @@ Parameters: "repository_size": 2066080, "lfs_objects_size": 0, "job_artifacts_size": 0 + }, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" } } ] @@ -257,6 +275,15 @@ Parameters: "repository_size": 1038090, "lfs_objects_size": 0, "job_artifacts_size": 0 + }, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" } }, { @@ -326,6 +353,15 @@ Parameters: "repository_size": 2066080, "lfs_objects_size": 0, "job_artifacts_size": 0 + }, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" } } ] @@ -427,6 +463,15 @@ Parameters: "repository_size": 1038090, "lfs_objects_size": 0, "job_artifacts_size": 0 + }, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" } } ``` @@ -659,7 +704,16 @@ Example response: "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, - "request_access_enabled": false + "request_access_enabled": false, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" + } } ``` @@ -725,7 +779,16 @@ Example response: "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, - "request_access_enabled": false + "request_access_enabled": false, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" + } } ``` @@ -809,7 +872,16 @@ Example response: "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, - "request_access_enabled": false + "request_access_enabled": false, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" + } } ``` @@ -893,7 +965,16 @@ Example response: "shared_with_groups": [], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, - "request_access_enabled": false + "request_access_enabled": false, + "_links": { + "self": "http://example.com/api/v4/projects", + "issues": "http://example.com/api/v4/projects/1/issues", + "merge_requests": "http://example.com/api/v4/projects/1/merge_requests", + "repo_branches": "http://example.com/api/v4/projects/1/repository_branches", + "labels": "http://example.com/api/v4/projects/1/labels", + "events": "http://example.com/api/v4/projects/1/events", + "members": "http://example.com/api/v4/projects/1/members" + } } ``` diff --git a/lib/api/api.rb b/lib/api/api.rb index efcf0976a81..9a983d31ac6 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -3,6 +3,7 @@ module API include APIGuard allow_access_with_scope :api + prefix :api version %w(v3 v4), using: :path diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 1719e9f7205..c165236105f 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -82,6 +82,38 @@ module API end class Project < Grape::Entity + include ::API::Helpers::RelatedResourcesHelpers + + expose :_links do + expose :self do |project| + expose_url(api_v4_projects_path(id: project.id)) + end + + expose :issues, if: -> (*args) { issues_available?(*args) } do |project| + expose_url(api_v4_projects_issues_path(id: project.id)) + end + + expose :merge_requests, if: -> (*args) { mrs_available?(*args) } do |project| + expose_url(api_v4_projects_merge_requests_path(id: project.id)) + end + + expose :repo_branches do |project| + expose_url(api_v4_projects_repository_branches_path(id: project.id)) + end + + expose :labels do |project| + expose_url(api_v4_projects_labels_path(id: project.id)) + end + + expose :events do |project| + expose_url(api_v4_projects_events_path(id: project.id)) + end + + expose :members do |project| + expose_url(api_v4_projects_members_path(id: project.id)) + end + end + expose :id, :description, :default_branch, :tag_list expose :archived?, as: :archived expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url @@ -297,6 +329,26 @@ module API end class Issue < IssueBasic + include ::API::Helpers::RelatedResourcesHelpers + + expose :_links do + expose :self do |issue| + expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid)) + end + + expose :notes do |issue| + expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid)) + end + + expose :award_emoji do |issue| + expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid)) + end + + expose :project do |issue| + expose_url(api_v4_projects_path(id: issue.project_id)) + end + end + expose :subscribed do |issue, options| issue.subscribed?(options[:current_user], options[:project] || issue.project) end diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb new file mode 100644 index 00000000000..769cc1457fc --- /dev/null +++ b/lib/api/helpers/related_resources_helpers.rb @@ -0,0 +1,28 @@ +module API + module Helpers + module RelatedResourcesHelpers + include GrapeRouteHelpers::NamedRouteMatcher + + def issues_available?(project, options) + available?(:issues, project, options[:current_user]) + end + + def mrs_available?(project, options) + available?(:merge_requests, project, options[:current_user]) + end + + def expose_url(path) + url_options = Rails.application.routes.default_url_options + protocol, host, port = url_options.slice(:protocol, :host, :port).values + + URI::HTTP.build(scheme: protocol, host: host, port: port, path: path).to_s + end + + private + + def available?(feature, project, current_user) + project.feature_available?(feature, current_user) + end + end + end +end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 14b26f28ebf..93ebe18508d 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -112,7 +112,7 @@ module API params do requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' end - get ":id/issues/:issue_iid" do + get ":id/issues/:issue_iid", as: :api_v4_project_issue do issue = find_project_issue(params[:issue_iid]) present issue, with: Entities::Issue, current_user: current_user, project: user_project end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 3759250f7f6..773f667abe0 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -259,11 +259,40 @@ module API expose :job_events, as: :build_events end - class Issue < ::API::Entities::Issue + class ProjectEntity < Grape::Entity + expose :id, :iid + expose(:project_id) { |entity| entity&.project.try(:id) } + expose :title, :description + expose :state, :created_at, :updated_at + end + + class IssueBasic < ProjectEntity + expose :label_names, as: :labels + expose :milestone, using: ::API::Entities::Milestone + expose :assignees, :author, using: ::API::Entities::UserBasic + + expose :assignee, using: ::API::Entities::UserBasic do |issue, options| + issue.assignees.first + end + + expose :user_notes_count + expose :upvotes, :downvotes + expose :due_date + expose :confidential + + expose :web_url do |issue, options| + Gitlab::UrlBuilder.build(issue) + end + end + + class Issue < IssueBasic unexpose :assignees expose :assignee do |issue, options| ::API::Entities::UserBasic.represent(issue.assignees.first, options) end + expose :subscribed do |issue, options| + issue.subscribed?(options[:current_user], options[:project] || issue.project) + end end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9837fedb522..ff4fc802176 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -693,6 +693,19 @@ describe API::Issues do expect(json_response['confidential']).to be_falsy end + context 'links exposure' do + it 'exposes related resources full URIs' do + get api("/projects/#{project.id}/issues/#{issue.iid}", user) + + links = json_response['_links'] + + expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}") + expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes") + expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji") + expect(links['project']).to end_with("/api/v4/projects/#{project.id}") + end + end + it "returns a project issue by internal id" do get api("/projects/#{project.id}/issues/#{issue.iid}", user) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 457f64cc88c..79e7e1a95df 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -815,6 +815,38 @@ describe API::Projects do expect(json_response).not_to include("import_error") end + context 'links exposure' do + it 'exposes related resources full URIs' do + get api("/projects/#{project.id}", user) + + links = json_response['_links'] + + expect(links['self']).to end_with("/api/v4/projects/#{project.id}") + expect(links['issues']).to end_with("/api/v4/projects/#{project.id}/issues") + expect(links['merge_requests']).to end_with("/api/v4/projects/#{project.id}/merge_requests") + expect(links['repo_branches']).to end_with("/api/v4/projects/#{project.id}/repository/branches") + expect(links['labels']).to end_with("/api/v4/projects/#{project.id}/labels") + expect(links['events']).to end_with("/api/v4/projects/#{project.id}/events") + expect(links['members']).to end_with("/api/v4/projects/#{project.id}/members") + end + + it 'filters related URIs when their feature is not enabled' do + project = create(:empty_project, :public, + :merge_requests_disabled, + :issues_disabled, + creator_id: user.id, + namespace: user.namespace) + + get api("/projects/#{project.id}", user) + + links = json_response['_links'] + + expect(links.has_key?('merge_requests')).to be_falsy + expect(links.has_key?('issues')).to be_falsy + expect(links['self']).to end_with("/api/v4/projects/#{project.id}") + end + end + describe 'permissions' do context 'all projects' do before do From 4236c2f055000ae9eadc165eea6355cf4825d595 Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Tue, 25 Jul 2017 10:46:01 +0100 Subject: [PATCH 069/143] Adds link_to_gfm method instrumentation --- changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml | 4 ++++ config/initializers/8_metrics.rb | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml diff --git a/changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml b/changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml new file mode 100644 index 00000000000..b5cf521561a --- /dev/null +++ b/changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml @@ -0,0 +1,4 @@ +--- +title: Add instrumentation to MarkupHelper#link_to_gfm +merge_request: 13069 +author: diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index 25630b298ce..2aeb94d47cd 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -114,6 +114,9 @@ def instrument_classes(instrumentation) # This is a Rails scope so we have to instrument it manually. instrumentation.instrument_method(Project, :visible_to_user) + # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/34509 + instrumentation.instrument_method(MarkupHelper, :link_to_gfm) + # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159 instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits) end From 33193155c7188209a168ceeb8ec4d3170dabbbaa Mon Sep 17 00:00:00 2001 From: winh Date: Mon, 24 Jul 2017 14:21:34 +0200 Subject: [PATCH 070/143] Make issuable searchbar full height --- app/assets/stylesheets/pages/issuable.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index aa04e490649..eb269df46fe 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -211,6 +211,10 @@ -webkit-overflow-scrolling: touch; } + &.affix-top .issuable-sidebar { + height: 100%; + } + &.right-sidebar-expanded { width: $gutter_width; From 531681c11d9e542fbd0d5ae5db8bc9a17cc0aefd Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 25 Jul 2017 11:28:30 +0100 Subject: [PATCH 071/143] Fix vertical alignment in firefox and safari for pipeline mini graph --- app/assets/stylesheets/pages/pipelines.scss | 3 ++- changelogs/unreleased/2971-multiproject-grah-ce-port.yml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/2971-multiproject-grah-ce-port.yml diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 9637d26e56d..d3862df20d3 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -597,7 +597,7 @@ } // Dropdown button in mini pipeline graph -.mini-pipeline-graph-dropdown-toggle { +button.mini-pipeline-graph-dropdown-toggle { border-radius: 100px; background-color: $white-light; border-width: 1px; @@ -608,6 +608,7 @@ padding: 0; transition: all 0.2s linear; position: relative; + vertical-align: middle; > .fa.fa-caret-down { position: absolute; diff --git a/changelogs/unreleased/2971-multiproject-grah-ce-port.yml b/changelogs/unreleased/2971-multiproject-grah-ce-port.yml new file mode 100644 index 00000000000..37584cac6ab --- /dev/null +++ b/changelogs/unreleased/2971-multiproject-grah-ce-port.yml @@ -0,0 +1,4 @@ +--- +title: Fix vertical alignment in firefox and safari for pipeline mini graph +merge_request: +author: From 77a6ec22ba9057154925a6484c05ae204423aacd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 25 Jul 2017 12:45:17 +0200 Subject: [PATCH 072/143] Handle maximum pages artifacts size correctly --- app/services/projects/update_pages_service.rb | 4 +- .../projects/update_pages_service_spec.rb | 72 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index e60b854f916..a819b799ff8 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -130,7 +130,9 @@ module Projects end def max_size - current_application_settings.max_pages_size.megabytes || MAX_SIZE + current_application_settings.max_pages_size.megabytes.tap do |maximum| + return MAX_SIZE if maximum.zero? || maximum > MAX_SIZE + end end def tmp_path diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index fc0a17296f3..8210f8a9608 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -96,6 +96,78 @@ describe Projects::UpdatePagesService do expect(execute).not_to eq(:success) end + describe 'maximum pages artifacts size' do + let(:metadata) { spy('metadata') } + + before do + file = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip') + metafile = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta') + + build.update_attributes(artifacts_file: file) + build.update_attributes(artifacts_metadata: metafile) + + allow(build).to receive(:artifacts_metadata_entry) + .and_return(metadata) + end + + shared_examples 'pages size limit exceeded' do + it 'limits the maximum size of gitlab pages' do + subject.execute + + expect(deploy_status.description) + .to match(/artifacts for pages are too large/) + end + end + + context 'when maximum pages size is set to zero' do + before do + stub_application_setting(max_pages_size: 0) + end + + context 'when page size does not exceed internal maximum' do + before do + allow(metadata).to receive(:total_size).and_return(200.megabytes) + end + + it 'updates pages correctly' do + subject.execute + + expect(deploy_status.description).not_to be_present + end + end + + context 'when pages size does exceed internal maximum' do + before do + allow(metadata).to receive(:total_size).and_return(2.terabytes) + end + + it_behaves_like 'pages size limit exceeded' + end + end + + context 'when pages size is greater than max size setting' do + before do + stub_application_setting(max_pages_size: 200) + allow(metadata).to receive(:total_size).and_return(201.megabytes) + end + + it_behaves_like 'pages size limit exceeded' + end + + context 'when max size setting is greater than internal max size' do + before do + stub_application_setting(max_pages_size: 3.terabytes / 1.megabyte) + allow(metadata).to receive(:total_size).and_return(2.terabytes) + end + + it_behaves_like 'pages size limit exceeded' + end + end + + def deploy_status + GenericCommitStatus.find_by(name: 'pages:deploy'); + end + def execute subject.execute[:status] end From 8758c10886c5a5dfc140a33142c3a0e15dc2b42b Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 25 Jul 2017 11:50:09 +0100 Subject: [PATCH 073/143] v3 API is unsupported after 9.5, but may not be removed That is, it may not _necessarily_ be removed. We do not provide guarantees for when API v3 will be available until beyond 9.5. --- doc/api/v3_to_v4.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 9db8e0351cf..9835fab7c98 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -2,9 +2,11 @@ Since GitLab 9.0, API V4 is the preferred version to be used. -API V3 will be removed in GitLab 9.5, to be released on August 22, 2017. In the -meantime, we advise you to make any necessary changes to applications that use -V3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md). +API V3 will be unsupported from GitLab 9.5, to be released on August +22, 2017. It will be removed in GitLab 9.5 or later. In the meantime, we advise +you to make any necessary changes to applications that use V3. The V3 API +documentation is still +[available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md). Below are the changes made between V3 and V4. From 8a50e5fb0c02e37c3eef80c88edfcb902571769c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 25 Jul 2017 13:04:22 +0200 Subject: [PATCH 074/143] Add changelog for max pages artifacts size fix --- .../fix-gb-handle-max-pages-artifacts-size-correctly.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-gb-handle-max-pages-artifacts-size-correctly.yml diff --git a/changelogs/unreleased/fix-gb-handle-max-pages-artifacts-size-correctly.yml b/changelogs/unreleased/fix-gb-handle-max-pages-artifacts-size-correctly.yml new file mode 100644 index 00000000000..3d9592bbf2a --- /dev/null +++ b/changelogs/unreleased/fix-gb-handle-max-pages-artifacts-size-correctly.yml @@ -0,0 +1,4 @@ +--- +title: Handle maximum pages artifacts size correctly +merge_request: 13072 +author: From b915a46758aa02bc5fa61fec02b1e80196a1b6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Tue, 25 Jul 2017 19:08:51 +0800 Subject: [PATCH 075/143] synchronize ukrainian translation in zanata again --- locale/uk/gitlab.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po index b81f566309c..c259ca253bc 100644 --- a/locale/uk/gitlab.po +++ b/locale/uk/gitlab.po @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language-Team: Ukrainian (https://translate.zanata.org/project/view/GitLab)\n" -"PO-Revision-Date: 2017-07-24 06:16-0400\n" +"PO-Revision-Date: 2017-07-25 03:27-0400\n" "Last-Translator: Андрей Витюк \n" "Language: uk\n" "X-Generator: Zanata 3.9.6\n" @@ -381,7 +381,7 @@ msgid "Edit" msgstr "Редагувати" msgid "Edit Pipeline Schedule %{id}" -msgstr "Редагувати Розклад Конвеєра % {id}" +msgstr "Редагувати Розклад Конвеєра %{id}" msgid "Every day (at 4:00am)" msgstr "Кожен день (в 4:00 ранку)" From 7151fb754b82888e022bfced02f2fdfd9000a1ff Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 25 Jul 2017 13:47:03 +0200 Subject: [PATCH 076/143] Fix rubocop offense in update pages service specs --- spec/services/projects/update_pages_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 8210f8a9608..aa6ad6340f5 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -165,7 +165,7 @@ describe Projects::UpdatePagesService do end def deploy_status - GenericCommitStatus.find_by(name: 'pages:deploy'); + GenericCommitStatus.find_by(name: 'pages:deploy') end def execute From 069a4a02e075548267266be2dcceb4002ba7be81 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Tue, 25 Jul 2017 06:33:00 +0000 Subject: [PATCH 077/143] Add directives to Vue component ordering --- doc/development/fe_guide/style_guide_js.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index ae844fa1051..149a0159680 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -447,6 +447,7 @@ A forEach will cause side effects, it will be mutating the array being iterated. 1. `name` 1. `props` 1. `mixins` + 1. `directives` 1. `data` 1. `components` 1. `computedProps` From a872c3e886528016d5383ef9260277b8120e2cc4 Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Mon, 24 Jul 2017 19:27:29 +0100 Subject: [PATCH 078/143] Bumps Gitlab Omniauth LDAP version --- Gemfile | 2 +- Gemfile.lock | 16 ++++++++-------- .../bump-omniauth-ldap-gem-version.yml | 4 ++++ 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/bump-omniauth-ldap-gem-version.yml diff --git a/Gemfile b/Gemfile index 5758b1b554e..d45c15fd650 100644 --- a/Gemfile +++ b/Gemfile @@ -60,7 +60,7 @@ gem 'browser', '~> 2.2' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master -gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap' +gem 'gitlab_omniauth-ldap', '~> 2.0.3', require: 'omniauth-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order diff --git a/Gemfile.lock b/Gemfile.lock index 6ffff0d8735..7b1d5dfdc6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -288,11 +288,11 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.1) - gitlab_omniauth-ldap (1.2.1) - net-ldap (~> 0.9) - omniauth (~> 1.0) - pyu-ruby-sasl (~> 0.0.3.1) - rubyntlm (~> 0.3) + gitlab_omniauth-ldap (2.0.3) + net-ldap (~> 0.16) + omniauth (~> 1.3) + pyu-ruby-sasl (>= 0.0.3.3, < 0.1) + rubyntlm (~> 0.5) globalid (0.3.7) activesupport (>= 4.1.0) gollum-grit_adapter (1.0.1) @@ -467,7 +467,7 @@ GEM mustermann-grape (1.0.0) mustermann (~> 1.0.0) mysql2 (0.4.5) - net-ldap (0.12.1) + net-ldap (0.16.0) netrc (0.11.0) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) @@ -740,7 +740,7 @@ GEM nokogiri (>= 1.5.10) ruby_parser (3.9.0) sexp_processor (~> 4.1) - rubyntlm (0.5.2) + rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.1) rufus-scheduler (3.4.0) @@ -974,7 +974,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) - gitlab_omniauth-ldap (~> 1.2.1) + gitlab_omniauth-ldap (~> 2.0.3) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.4) gon (~> 6.1.0) diff --git a/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml b/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml new file mode 100644 index 00000000000..42e1c9e8f83 --- /dev/null +++ b/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml @@ -0,0 +1,4 @@ +--- +title: Prevent LDAP login callback from being called with a GET request +merge_request: 13059 +author: From 5fdef68f2bb35dc7a217c55cd6f1ed01ec3adff2 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Tue, 25 Jul 2017 14:14:28 +0200 Subject: [PATCH 079/143] Move relative_path to the element that is being clicked --- .../shared/_new_project_item_select.html.haml | 4 ++-- spec/features/dashboard/issues_spec.rb | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index c1acee1a211..5f3cdaefd54 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,6 +1,6 @@ - if @projects.any? .project-item-select-holder - = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }, with_feature_enabled: local_assigns[:with_feature_enabled] - %a.btn.btn-new.new-project-item-select-button{ data: { relative_path: local_assigns[:path] } } + = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled] + %a.btn.btn-new.new-project-item-select-button = local_assigns[:label] = icon('caret-down') diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 69c1a2ed89a..2a5ef08da60 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -78,5 +78,23 @@ RSpec.describe 'Dashboard Issues', feature: true do expect(page).not_to have_content(project_with_issues_disabled.name_with_namespace) end end + + it 'shows the new issue page', js: true do + Gitlab::Application.routes.default_url_options = { + host: Capybara.current_session.server.host, + port: Capybara.current_session.server.port, + protocol: 'http' + } + + find('.new-project-item-select-button').trigger('click') + wait_for_requests + find('.select2-results li').click + + expect(page).to have_current_path("/#{project.path_with_namespace}/issues/new") + + page.within('#content-body') do + expect(page).to have_selector('.issue-form') + end + end end end From 2286879583580861109207c05aa4fae1729a02b6 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 25 Jul 2017 14:19:09 +0200 Subject: [PATCH 080/143] Ensure test files are deleted after tests --- config/initializers/7_prometheus_metrics.rb | 2 +- .../health_checks/base_abstract_check.rb | 6 +- lib/gitlab/health_checks/fs_shards_check.rb | 67 ++++++++++++------- .../health_checks/fs_shards_check_spec.rb | 6 ++ 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index 987324a86c9..a2f8421f5d7 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -6,7 +6,7 @@ Prometheus::Client.configure do |config| config.initial_mmap_file_size = 4 * 1024 config.multiprocess_files_dir = ENV['prometheus_multiproc_dir'] - if Rails.env.development? && Rails.env.test? + if Rails.env.development? || Rails.env.test? config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir') end end diff --git a/lib/gitlab/health_checks/base_abstract_check.rb b/lib/gitlab/health_checks/base_abstract_check.rb index 7de6d4d9367..8b365dab185 100644 --- a/lib/gitlab/health_checks/base_abstract_check.rb +++ b/lib/gitlab/health_checks/base_abstract_check.rb @@ -27,10 +27,10 @@ module Gitlab Metric.new(name, value, labels) end - def with_timing(proc) + def with_timing start = Time.now - result = proc.call - yield result, Time.now.to_f - start.to_f + result = yield + [result, Time.now.to_f - start.to_f] end def catch_timeout(seconds, &block) diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index bebde857b16..45cd8f7eefc 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -10,52 +10,64 @@ module Gitlab def readiness repository_storages.map do |storage_name| begin - tmp_file_path = tmp_file_path(storage_name) - if !storage_stat_test(storage_name) HealthChecks::Result.new(false, 'cannot stat storage', shard: storage_name) - elsif !storage_write_test(tmp_file_path) - HealthChecks::Result.new(false, 'cannot write to storage', shard: storage_name) - elsif !storage_read_test(tmp_file_path) - HealthChecks::Result.new(false, 'cannot read from storage', shard: storage_name) else - HealthChecks::Result.new(true, nil, shard: storage_name) + with_temp_file(storage_name) do |tmp_file_path| + if !storage_write_test(tmp_file_path) + HealthChecks::Result.new(false, 'cannot write to storage', shard: storage_name) + elsif !storage_read_test(tmp_file_path) + HealthChecks::Result.new(false, 'cannot read from storage', shard: storage_name) + else + HealthChecks::Result.new(true, nil, shard: storage_name) + end + end end rescue RuntimeError => ex message = "unexpected error #{ex} when checking storage #{storage_name}" Rails.logger.error(message) HealthChecks::Result.new(false, message, shard: storage_name) - ensure - delete_test_file(tmp_file_path) end end end def metrics - repository_storages.flat_map do |storage_name| - tmp_file_path = tmp_file_path(storage_name) - [ - operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, -> { storage_stat_test(storage_name) }, shard: storage_name), - operation_metrics(:filesystem_writable, :filesystem_write_latency_seconds, -> { storage_write_test(tmp_file_path) }, shard: storage_name), - operation_metrics(:filesystem_readable, :filesystem_read_latency_seconds, -> { storage_read_test(tmp_file_path) }, shard: storage_name) - ].flatten + res = [] + repository_storages.each do |storage_name| + res << operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, shard: storage_name) do + with_timing { storage_stat_test(storage_name) } + end + + res << operation_metrics(:filesystem_writable, :filesystem_write_latency_seconds, shard: storage_name) do + with_temp_file(storage_name) do |tmp_file_path| + with_timing { storage_write_test(tmp_file_path) } + end + end + + res << operation_metrics(:filesystem_readable, :filesystem_read_latency_seconds, shard: storage_name) do + with_temp_file(storage_name) do |tmp_file_path| + storage_write_test(tmp_file_path) # writes data used by read test + with_timing { storage_read_test(tmp_file_path) } + end + end end + res.flatten end private - def operation_metrics(ok_metric, latency_metric, operation, **labels) - with_timing operation do |result, elapsed| - [ - metric(latency_metric, elapsed, **labels), - metric(ok_metric, result ? 1 : 0, **labels) - ] - end + def operation_metrics(ok_metric, latency_metric, **labels) + result, elapsed = yield + [ + metric(latency_metric, elapsed, **labels), + metric(ok_metric, result ? 1 : 0, **labels) + ] rescue RuntimeError => ex Rails.logger.error("unexpected error #{ex} when checking #{ok_metric}") [metric(ok_metric, 0, **labels)] end + def repository_storages @repository_storage ||= Gitlab::CurrentSettings.current_application_settings.repository_storages end @@ -68,8 +80,13 @@ module Gitlab Gitlab::Popen.popen([TIMEOUT_EXECUTABLE, COMMAND_TIMEOUT].concat(cmd_args), *args, &block) end - def tmp_file_path(storage_name) - Dir::Tmpname.create(%w(fs_shards_check +deleted), path(storage_name)) { |path| path } + def with_temp_file(storage_name) + begin + temp_file_path = Dir::Tmpname.create(%w(fs_shards_check +deleted), path(storage_name)) { |path| path } + yield temp_file_path + ensure + delete_test_file(temp_file_path) + end end def path(storage_name) diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 3de73a9ff65..947fb1b64d0 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -131,6 +131,12 @@ describe Gitlab::HealthChecks::FsShardsCheck do expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) end + + it 'cleans up files used for metrics' do + subject + + expect(Dir.entries(tmp_dir).count).to eq(2) + end end end end From b1d6670d049c9645ddf4def369de0b12521692b5 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 25 Jul 2017 14:30:27 +0200 Subject: [PATCH 081/143] Add Changelog about temp file removal fix + remove whitespace --- ...awel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml | 4 ++++ lib/gitlab/health_checks/fs_shards_check.rb | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/pawel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml diff --git a/changelogs/unreleased/pawel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml b/changelogs/unreleased/pawel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml new file mode 100644 index 00000000000..4906232237f --- /dev/null +++ b/changelogs/unreleased/pawel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml @@ -0,0 +1,4 @@ +--- +title: Ensure fs metrics test files are deleted +merge_request: +author: diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index 45cd8f7eefc..ab18701b3bf 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -67,7 +67,6 @@ module Gitlab [metric(ok_metric, 0, **labels)] end - def repository_storages @repository_storage ||= Gitlab::CurrentSettings.current_application_settings.repository_storages end From ad46c8878b3102f74e211ef72ff5347b89aee14c Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 25 Jul 2017 13:48:30 +0200 Subject: [PATCH 082/143] Add `api` prefix as a top level route in the spec. Now that it has been removed from the rails routes. But it still needs to be a reserved top-level word, so the tests should know about this. --- spec/lib/gitlab/path_regex_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index c38bbb64fc3..20be743d224 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -86,7 +86,7 @@ describe Gitlab::PathRegex, lib: true do route.split('/')[1] end.compact.uniq - words + ee_top_level_words + files_in_public + words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s) end let(:ee_top_level_words) do From 37f27079fe16ffb6f8dbb888593335a361f5964a Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 25 Jul 2017 15:17:05 +0200 Subject: [PATCH 083/143] Fix redis check with_timing method usage --- lib/gitlab/health_checks/simple_abstract_check.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/health_checks/simple_abstract_check.rb b/lib/gitlab/health_checks/simple_abstract_check.rb index 3dcb28a193c..f5026171ba4 100644 --- a/lib/gitlab/health_checks/simple_abstract_check.rb +++ b/lib/gitlab/health_checks/simple_abstract_check.rb @@ -15,14 +15,13 @@ module Gitlab end def metrics - with_timing method(:check) do |result, elapsed| - Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless is_successful?(result) - [ - metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0), - metric("#{metric_prefix}_success", is_successful?(result) ? 1 : 0), - metric("#{metric_prefix}_latency_seconds", elapsed) - ] - end + result, elapsed = with_timing(&method(:check)) + Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless is_successful?(result) + [ + metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0), + metric("#{metric_prefix}_success", is_successful?(result) ? 1 : 0), + metric("#{metric_prefix}_latency_seconds", elapsed) + ] end private From a78306e7fa0e815a5586a81ee9c2fcf095793de4 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 25 Jul 2017 13:59:50 +0200 Subject: [PATCH 084/143] Enable gitaly_post_upload_pack by default --- changelogs/unreleased/post-upload-pack-opt-out.yml | 4 ++++ lib/gitlab/workhorse.rb | 5 ++++- spec/lib/gitlab/workhorse_spec.rb | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/post-upload-pack-opt-out.yml diff --git a/changelogs/unreleased/post-upload-pack-opt-out.yml b/changelogs/unreleased/post-upload-pack-opt-out.yml new file mode 100644 index 00000000000..302a99795a0 --- /dev/null +++ b/changelogs/unreleased/post-upload-pack-opt-out.yml @@ -0,0 +1,4 @@ +--- +title: Enable gitaly_post_upload_pack by default +merge_request: 13078 +author: diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 5dd8a38fea2..3f25e463412 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -35,7 +35,10 @@ module Gitlab when 'git_receive_pack' Gitlab::GitalyClient.feature_enabled?(:post_receive_pack) when 'git_upload_pack' - Gitlab::GitalyClient.feature_enabled?(:post_upload_pack) + Gitlab::GitalyClient.feature_enabled?( + :post_upload_pack, + status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT + ) when 'info_refs' true else diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 7b39441e76e..6ca1edb01b9 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -237,7 +237,8 @@ describe Gitlab::Workhorse, lib: true do context 'when action is not enabled by feature flag' do it 'does not include Gitaly params in the returned value' do - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(false) + status_opt_out = Gitlab::GitalyClient::MigrationStatus::OPT_OUT + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag, status: status_opt_out).and_return(false) expect(subject).not_to include(gitaly_params) end From 3f59e354a7324e9bf332a34661743d85e82b987c Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 25 Jul 2017 15:28:13 +0100 Subject: [PATCH 085/143] Update CHANGELOG.md for 9.4.1 [ci skip] --- CHANGELOG.md | 12 ++++++++++++ .../unreleased/35399-mini-graph-commits-box.yml | 4 ---- ...4-error-500-viewing-notes-with-anonymous-user.yml | 4 ---- changelogs/unreleased/bvl-fix-invalid-po-files.yml | 4 ---- .../bvl-fix-login-issue-with-ldap-enabled.yml | 5 ----- .../fix-gb-project-update-with-registry-images.yml | 4 ---- ...0-pipeline_schedules-pages-throwing-error-500.yml | 4 ---- .../unreleased/issue-boards-close-icon-size.yml | 4 ---- .../new-nav-duplicated-new-milestone-buttons.yml | 4 ---- .../unreleased/pawel-fix-metrics-files-handling.yml | 4 ---- 10 files changed, 12 insertions(+), 37 deletions(-) delete mode 100644 changelogs/unreleased/35399-mini-graph-commits-box.yml delete mode 100644 changelogs/unreleased/35444-error-500-viewing-notes-with-anonymous-user.yml delete mode 100644 changelogs/unreleased/bvl-fix-invalid-po-files.yml delete mode 100644 changelogs/unreleased/bvl-fix-login-issue-with-ldap-enabled.yml delete mode 100644 changelogs/unreleased/fix-gb-project-update-with-registry-images.yml delete mode 100644 changelogs/unreleased/fix-sm-32790-pipeline_schedules-pages-throwing-error-500.yml delete mode 100644 changelogs/unreleased/issue-boards-close-icon-size.yml delete mode 100644 changelogs/unreleased/new-nav-duplicated-new-milestone-buttons.yml delete mode 100644 changelogs/unreleased/pawel-fix-metrics-files-handling.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index daf154eeb07..580d2357512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.4.1 (2017-07-25) + +- Fix pipeline_schedules pages throwing error 500 (when ref is empty). !12983 +- Fix editing project with container images present. !13028 +- Fix some invalid entries in PO files. !13032 +- Fix cross site request protection when logging in as a regular user when LDAP is enabled. !13049 +- Fix bug causing metrics files to be truncated. !35420 +- Fix anonymous access to public projects in groups with pending invites. +- Fixed issue boards sidebar close icon size. +- Fixed duplicate new milestone buttons when new navigation is turned on. +- Fix margins in the mini graph for pipeline in commits box. + ## 9.4.0 (2017-07-22) - Add blame view age mapping. !7198 (Jeff Stubler) diff --git a/changelogs/unreleased/35399-mini-graph-commits-box.yml b/changelogs/unreleased/35399-mini-graph-commits-box.yml deleted file mode 100644 index ed080ed86b4..00000000000 --- a/changelogs/unreleased/35399-mini-graph-commits-box.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix margins in the mini graph for pipeline in commits box -merge_request: -author: diff --git a/changelogs/unreleased/35444-error-500-viewing-notes-with-anonymous-user.yml b/changelogs/unreleased/35444-error-500-viewing-notes-with-anonymous-user.yml deleted file mode 100644 index 9b8bc1d0d99..00000000000 --- a/changelogs/unreleased/35444-error-500-viewing-notes-with-anonymous-user.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix anonymous access to public projects in groups with pending invites -merge_request: -author: diff --git a/changelogs/unreleased/bvl-fix-invalid-po-files.yml b/changelogs/unreleased/bvl-fix-invalid-po-files.yml deleted file mode 100644 index b8a22a9e6df..00000000000 --- a/changelogs/unreleased/bvl-fix-invalid-po-files.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix some invalid entries in PO files -merge_request: 13032 -author: diff --git a/changelogs/unreleased/bvl-fix-login-issue-with-ldap-enabled.yml b/changelogs/unreleased/bvl-fix-login-issue-with-ldap-enabled.yml deleted file mode 100644 index a98455d0916..00000000000 --- a/changelogs/unreleased/bvl-fix-login-issue-with-ldap-enabled.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix cross site request protection when logging in as a regular user when LDAP - is enabled -merge_request: 13049 -author: diff --git a/changelogs/unreleased/fix-gb-project-update-with-registry-images.yml b/changelogs/unreleased/fix-gb-project-update-with-registry-images.yml deleted file mode 100644 index a54a34c71d4..00000000000 --- a/changelogs/unreleased/fix-gb-project-update-with-registry-images.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix editing project with container images present -merge_request: 13028 -author: diff --git a/changelogs/unreleased/fix-sm-32790-pipeline_schedules-pages-throwing-error-500.yml b/changelogs/unreleased/fix-sm-32790-pipeline_schedules-pages-throwing-error-500.yml deleted file mode 100644 index 334d8ca4d9e..00000000000 --- a/changelogs/unreleased/fix-sm-32790-pipeline_schedules-pages-throwing-error-500.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline_schedules pages throwing error 500 (when ref is empty) -merge_request: 12983 -author: diff --git a/changelogs/unreleased/issue-boards-close-icon-size.yml b/changelogs/unreleased/issue-boards-close-icon-size.yml deleted file mode 100644 index bc6bda0e50d..00000000000 --- a/changelogs/unreleased/issue-boards-close-icon-size.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards sidebar close icon size -merge_request: -author: diff --git a/changelogs/unreleased/new-nav-duplicated-new-milestone-buttons.yml b/changelogs/unreleased/new-nav-duplicated-new-milestone-buttons.yml deleted file mode 100644 index fcf7d8e63d6..00000000000 --- a/changelogs/unreleased/new-nav-duplicated-new-milestone-buttons.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed duplicate new milestone buttons when new navigation is turned on -merge_request: -author: diff --git a/changelogs/unreleased/pawel-fix-metrics-files-handling.yml b/changelogs/unreleased/pawel-fix-metrics-files-handling.yml deleted file mode 100644 index cfdb4246af9..00000000000 --- a/changelogs/unreleased/pawel-fix-metrics-files-handling.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix bug causing metrics files to be truncated -merge_request: 35420 -author: From 1c572994004acbd442c05537cb5062cd2e5d29e6 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Tue, 25 Jul 2017 17:25:41 +0200 Subject: [PATCH 086/143] Remove project_key from the Jira configuration --- app/models/project_services/jira_service.rb | 18 +++------------- .../31129-jira-project-key-elim.yml | 4 ++++ doc/user/project/integrations/jira.md | 4 ++-- features/steps/project/services.rb | 1 - lib/api/services.rb | 6 ------ .../projects/services/jira_service_spec.rb | 21 +++++++------------ .../project_services/jira_service_spec.rb | 10 +++------ spec/support/jira_service_helper.rb | 2 +- 8 files changed, 20 insertions(+), 46 deletions(-) create mode 100644 changelogs/unreleased/31129-jira-project-key-elim.yml diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 450027c2e57..37f2c96a22f 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -3,10 +3,8 @@ class JiraService < IssueTrackerService validates :url, url: true, presence: true, if: :activated? validates :api_url, url: true, allow_blank: true - validates :project_key, presence: true, if: :activated? - prop_accessor :username, :password, :url, :api_url, :project_key, - :jira_issue_transition_id, :title, :description + prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description before_update :reset_password @@ -54,10 +52,6 @@ class JiraService < IssueTrackerService @client ||= JIRA::Client.new(options) end - def jira_project - @jira_project ||= jira_request { client.Project.find(project_key) } - end - def help "You need to configure JIRA before enabling this service. For more details read the @@ -88,18 +82,12 @@ class JiraService < IssueTrackerService [ { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true }, { type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' }, - { type: 'text', name: 'project_key', placeholder: 'Project Key', required: true }, { type: 'text', name: 'username', placeholder: '', required: true }, { type: 'password', name: 'password', placeholder: '', required: true }, - { type: 'text', name: 'jira_issue_transition_id', placeholder: '' } + { type: 'text', name: 'jira_issue_transition_id', title: 'Transition ID', placeholder: '' } ] end - # URLs to redirect from Gitlab issues pages to jira issue tracker - def project_url - "#{url}/issues/?jql=project=#{project_key}" - end - def issues_url "#{url}/browse/:id" end @@ -184,7 +172,7 @@ class JiraService < IssueTrackerService def test_settings return unless client_url.present? # Test settings by getting the project - jira_request { jira_project.present? } + jira_request { client.ServerInfo.all.attrs } end private diff --git a/changelogs/unreleased/31129-jira-project-key-elim.yml b/changelogs/unreleased/31129-jira-project-key-elim.yml new file mode 100644 index 00000000000..bfa0e99f250 --- /dev/null +++ b/changelogs/unreleased/31129-jira-project-key-elim.yml @@ -0,0 +1,4 @@ +--- +title: Remove project_key from the Jira configuration +merge_request: 12050 +author: diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index cf03f2a9033..cfa4c8a93f8 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -98,11 +98,11 @@ in the table below. | Field | Description | | ----- | ----------- | | `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. | -| `JIRA API URL` | The base URL to the JIRA instance API. E.g., `https://jira-api.example.com`. This is optional. If not entered, the Web URL value be used. | +| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | | `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** | +| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** | After saving the configuration, your GitLab project will be able to interact with all JIRA projects in your JIRA instance. diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 906a81b29b3..7e2a357f6b2 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -175,7 +175,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps fill_in 'JIRA API URL', with: 'http://jira.example/api' fill_in 'Username', with: 'gitlab' fill_in 'Password', with: 'gitlab' - fill_in 'Project Key', with: 'GITLAB' click_button 'Save' end diff --git a/lib/api/services.rb b/lib/api/services.rb index 7488f95a9b7..843c05ae32e 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -312,12 +312,6 @@ module API type: String, desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' }, - { - required: true, - name: :project_key, - type: String, - desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' - }, { required: false, name: :username, diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb index 7c29af247d6..b71eec0ecfd 100644 --- a/spec/features/projects/services/jira_service_spec.rb +++ b/spec/features/projects/services/jira_service_spec.rb @@ -6,17 +6,12 @@ feature 'Setup Jira service', :feature, :js do let(:service) { project.create_jira_service } let(:url) { 'http://jira.example.com' } - - def stub_project_url - WebMock.stub_request(:get, 'http://jira.example.com/rest/api/2/project/GitLabProject') - .with(basic_auth: %w(username password)) - end + let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' } def fill_form(active = true) check 'Active' if active fill_in 'service_url', with: url - fill_in 'service_project_key', with: 'GitLabProject' fill_in 'service_username', with: 'username' fill_in 'service_password', with: 'password' fill_in 'service_jira_issue_transition_id', with: '25' @@ -31,11 +26,10 @@ feature 'Setup Jira service', :feature, :js do describe 'user sets and activates Jira Service' do context 'when Jira connection test succeeds' do - before do - stub_project_url - end - it 'activates the JIRA service' do + server_info = { key: 'value' }.to_json + WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info) + click_link('JIRA') fill_form click_button('Test settings and save changes') @@ -47,10 +41,6 @@ feature 'Setup Jira service', :feature, :js do end context 'when Jira connection test fails' do - before do - stub_project_url.to_return(status: 401) - end - it 'shows errors when some required fields are not filled in' do click_link('JIRA') @@ -64,6 +54,9 @@ feature 'Setup Jira service', :feature, :js do end it 'activates the JIRA service' do + WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)) + .to_raise(JIRA::HTTPError.new(double(message: 'message'))) + click_link('JIRA') fill_form click_button('Test settings and save changes') diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 105afed1337..d7d09808a98 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -15,7 +15,6 @@ describe JiraService, models: true do end it { is_expected.to validate_presence_of(:url) } - it { is_expected.to validate_presence_of(:project_key) } it_behaves_like 'issue tracker service URL attribute', :url end @@ -34,7 +33,6 @@ describe JiraService, models: true do active: true, username: 'username', password: 'test', - project_key: 'TEST', jira_issue_transition_id: 24, url: 'http://jira.test.com' ) @@ -88,7 +86,6 @@ describe JiraService, models: true do url: 'http://jira.example.com', username: 'gitlab_jira_username', password: 'gitlab_jira_password', - project_key: 'GitLabProject', jira_issue_transition_id: "custom-id" ) @@ -196,15 +193,14 @@ describe JiraService, models: true do project: create(:project), url: 'http://jira.example.com', username: 'jira_username', - password: 'jira_password', - project_key: 'GitLabProject' + password: 'jira_password' ) end def test_settings(api_url) - project_url = "http://#{api_url}/rest/api/2/project/GitLabProject" + test_url = "http://#{api_url}/rest/api/2/serverInfo" - WebMock.stub_request(:get, project_url).with(basic_auth: %w(jira_username jira_password)) + WebMock.stub_request(:get, test_url).with(basic_auth: %w(jira_username jira_password)).to_return(body: { url: 'http://url' }.to_json ) jira_service.test_settings end diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index 97ae0b6afc5..0b5f66597fd 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -51,7 +51,7 @@ module JiraServiceHelper end def jira_project_url - JIRA_API + "/project/#{jira_tracker.project_key}" + JIRA_API + "/project" end def jira_api_comment_url(issue_id) From 6263ecd3a42312a62957674665e35d3590192123 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 24 Jul 2017 15:09:55 -0700 Subject: [PATCH 087/143] Add lower path index to redirect_routes --- ...dd-lower-path-index-to-redirect-routes.yml | 4 +++ ...add_lower_path_index_to_redirect_routes.rb | 34 +++++++++++++++++++ db/schema.rb | 2 +- lib/tasks/migrate/setup_postgresql.rake | 2 ++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/mk-add-lower-path-index-to-redirect-routes.yml create mode 100644 db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb diff --git a/changelogs/unreleased/mk-add-lower-path-index-to-redirect-routes.yml b/changelogs/unreleased/mk-add-lower-path-index-to-redirect-routes.yml new file mode 100644 index 00000000000..37a5fa66d13 --- /dev/null +++ b/changelogs/unreleased/mk-add-lower-path-index-to-redirect-routes.yml @@ -0,0 +1,4 @@ +--- +title: Improve redirect route query performance +merge_request: 13062 +author: diff --git a/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb b/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb new file mode 100644 index 00000000000..db60c2087b9 --- /dev/null +++ b/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb @@ -0,0 +1,34 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddLowerPathIndexToRedirectRoutes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_on_redirect_routes_lower_path' + + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (LOWER(path));" + end + + def down + return unless Gitlab::Database.postgresql? + + # Why not use remove_concurrent_index_by_name? + # + # `index_exists?` doesn't work on this index. Perhaps this is related to the + # fact that the index doesn't show up in the schema. And apparently it isn't + # trivial to write a query that checks for an index. BUT there is a + # convenient `IF EXISTS` parameter for `DROP INDEX`. + if supports_drop_index_concurrently? + disable_statement_timeout + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" + else + execute "DROP INDEX IF EXISTS #{INDEX_NAME};" + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 284b2068166..7724af5b610 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170717150329) do +ActiveRecord::Schema.define(version: 20170724214302) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index 4108cee08b4..9cc986535e1 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -4,6 +4,7 @@ require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lowe require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes') require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes') require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like') +require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes') require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like') desc 'GitLab | Sets up PostgreSQL' @@ -12,5 +13,6 @@ task setup_postgresql: :environment do AddUsersLowerUsernameEmailIndexes.new.up AddLowerPathIndexToRoutes.new.up IndexRoutesPathForLike.new.up + AddLowerPathIndexToRedirectRoutes.new.up IndexRedirectRoutesPathForLike.new.up end From 22d53f06076e52165af3ba04d0b703bed20cfb97 Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Tue, 25 Jul 2017 10:09:21 +0100 Subject: [PATCH 088/143] Fixes 500 error caused by pending delete projects in admin dashboard --- app/controllers/admin/dashboard_controller.rb | 2 +- ...-projects-error-in-admin-dashboard-fix.yml | 4 ++++ .../admin/dashboard_controller_spec.rb | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/35453-pending-delete-projects-error-in-admin-dashboard-fix.yml create mode 100644 spec/controllers/admin/dashboard_controller_spec.rb diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 8360ce08bdc..05e749c00c0 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -1,6 +1,6 @@ class Admin::DashboardController < Admin::ApplicationController def index - @projects = Project.with_route.limit(10) + @projects = Project.without_deleted.with_route.limit(10) @users = User.limit(10) @groups = Group.with_route.limit(10) end diff --git a/changelogs/unreleased/35453-pending-delete-projects-error-in-admin-dashboard-fix.yml b/changelogs/unreleased/35453-pending-delete-projects-error-in-admin-dashboard-fix.yml new file mode 100644 index 00000000000..fa906accbb8 --- /dev/null +++ b/changelogs/unreleased/35453-pending-delete-projects-error-in-admin-dashboard-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fixes 500 error caused by pending delete projects in admin dashboard +merge_request: 13067 +author: diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/controllers/admin/dashboard_controller_spec.rb new file mode 100644 index 00000000000..6eb9f7867d5 --- /dev/null +++ b/spec/controllers/admin/dashboard_controller_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Admin::DashboardController do + describe '#index' do + context 'with pending_delete projects' do + render_views + + it 'does not retrieve projects that are pending deletion' do + sign_in(create(:admin)) + + project = create(:project) + pending_delete_project = create(:project, pending_delete: true) + + get :index + + expect(response.body).to match(project.name) + expect(response.body).not_to match(pending_delete_project.name) + end + end + end +end From 96479cba5ac7d9b6a4b3364a29037e4e83fec25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 25 Jul 2017 19:00:49 +0200 Subject: [PATCH 089/143] Remove outdated ~Frontend label in CONTRIBUTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a8499c126aa..12fb34b24be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,8 +114,8 @@ scheduling into milestones. Labelling is a task for everyone. Most issues will have labels for at least one of the following: - Type: ~"feature proposal", ~bug, ~customer, etc. -- Subject: ~wiki, ~"container registry", ~ldap, ~api, etc. -- Team: ~CI, ~Discussion, ~Edge, ~Frontend, ~Platform, etc. +- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. +- Team: ~CI, ~Discussion, ~Edge, ~Platform, etc. - Priority: ~Deliverable, ~Stretch All labels, their meaning and priority are defined on the @@ -278,7 +278,7 @@ For feature proposals for EE, open an issue on the In order to help track the feature proposals, we have created a [`feature proposal`][fpl] label. For the time being, users that are not members of the project cannot add labels. You can instead ask one of the [core team] -members to add the label `feature proposal` to the issue or add the following +members to add the label ~"feature proposal" to the issue or add the following code snippet right after your description in a new line: `~"feature proposal"`. Please keep feature proposals as small and simple as possible, complex ones From e4028988b4f8428609abca36c92bfeca747bbefd Mon Sep 17 00:00:00 2001 From: Chenjerai Katanda Date: Tue, 25 Jul 2017 17:23:39 +0000 Subject: [PATCH 090/143] Add note on external url setting for HA clusters. --- doc/administration/high_availability/gitlab.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md index 137fed35a73..e2cf281bc49 100644 --- a/doc/administration/high_availability/gitlab.md +++ b/doc/administration/high_availability/gitlab.md @@ -70,6 +70,12 @@ for each GitLab application server in your environment. gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server gitlab_rails['redis_password'] = 'Redis Password' ``` + + > **Note:** To maintain uniformity of links across HA clusters, the `external_url` + on the master as well as all secondary application servers should point to the + eventual url that users will use to access GitLab. In a typical HA setup, + this will be the url of the load balancer which will route traffic to all + GitLab application servers in the HA cluster. 1. Run `sudo gitlab-ctl reconfigure` to compile the configuration. From 250dbecd28473ce256e46f7233c14acb8c02a29d Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Tue, 25 Jul 2017 18:15:45 +0100 Subject: [PATCH 091/143] Pending delete projects should not show in deploy keys --- app/serializers/deploy_key_entity.rb | 2 +- ...38-deploy-keys-should-not-show-pending-delete-projects.yml | 4 ++++ spec/serializers/deploy_key_entity_spec.rb | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/35338-deploy-keys-should-not-show-pending-delete-projects.yml diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb index 068013c8829..c75431a79ae 100644 --- a/app/serializers/deploy_key_entity.rb +++ b/app/serializers/deploy_key_entity.rb @@ -9,7 +9,7 @@ class DeployKeyEntity < Grape::Entity expose :created_at expose :updated_at expose :projects, using: ProjectEntity do |deploy_key| - deploy_key.projects.select { |project| options[:user].can?(:read_project, project) } + deploy_key.projects.without_deleted.select { |project| options[:user].can?(:read_project, project) } end expose :can_edit diff --git a/changelogs/unreleased/35338-deploy-keys-should-not-show-pending-delete-projects.yml b/changelogs/unreleased/35338-deploy-keys-should-not-show-pending-delete-projects.yml new file mode 100644 index 00000000000..73808030f4c --- /dev/null +++ b/changelogs/unreleased/35338-deploy-keys-should-not-show-pending-delete-projects.yml @@ -0,0 +1,4 @@ +--- +title: Pending delete projects should not show in deploy keys. +merge_request: 13088 +author: diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb index 9620f9665cf..8149de869f1 100644 --- a/spec/serializers/deploy_key_entity_spec.rb +++ b/spec/serializers/deploy_key_entity_spec.rb @@ -2,13 +2,15 @@ require 'spec_helper' describe DeployKeyEntity do include RequestAwareEntity - + let(:user) { create(:user) } let(:project) { create(:empty_project, :internal)} let(:project_private) { create(:empty_project, :private)} + let!(:project_pending_delete) { create(:empty_project, :internal, pending_delete: true) } let(:deploy_key) { create(:deploy_key) } let!(:deploy_key_internal) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) } let!(:deploy_key_private) { create(:deploy_keys_project, project: project_private, deploy_key: deploy_key) } + let!(:deploy_key_pending_delete) { create(:deploy_keys_project, project: project_pending_delete, deploy_key: deploy_key) } let(:entity) { described_class.new(deploy_key, user: user) } From 895e1b3ed1de5f94414b0e042b0053fab794a1f6 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Wed, 26 Jul 2017 00:28:13 +0200 Subject: [PATCH 092/143] Stop abusing subject to store results, + add helper methods to cleanup fs_shards metrics --- ..._files_are_deleted_in_fs_metrics-35457.yml | 2 +- lib/gitlab/health_checks/fs_shards_check.rb | 64 +++++++++++-------- .../health_checks/fs_shards_check_spec.rb | 59 ++++++++--------- 3 files changed, 64 insertions(+), 61 deletions(-) diff --git a/changelogs/unreleased/pawel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml b/changelogs/unreleased/pawel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml index 4906232237f..1186dc59dc7 100644 --- a/changelogs/unreleased/pawel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml +++ b/changelogs/unreleased/pawel-ensure_temp_files_are_deleted_in_fs_metrics-35457.yml @@ -1,4 +1,4 @@ --- -title: Ensure fs metrics test files are deleted +title: Ensure filesystem metrics test files are deleted merge_request: author: diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index ab18701b3bf..ddd1aaa7043 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -32,26 +32,13 @@ module Gitlab end def metrics - res = [] - repository_storages.each do |storage_name| - res << operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, shard: storage_name) do - with_timing { storage_stat_test(storage_name) } - end - - res << operation_metrics(:filesystem_writable, :filesystem_write_latency_seconds, shard: storage_name) do - with_temp_file(storage_name) do |tmp_file_path| - with_timing { storage_write_test(tmp_file_path) } - end - end - - res << operation_metrics(:filesystem_readable, :filesystem_read_latency_seconds, shard: storage_name) do - with_temp_file(storage_name) do |tmp_file_path| - storage_write_test(tmp_file_path) # writes data used by read test - with_timing { storage_read_test(tmp_file_path) } - end - end + repository_storages.flat_map do |storage_name| + [ + storage_stat_metrics(storage_name), + storage_write_metrics(storage_name), + storage_read_metrics(storage_name) + ].flatten end - res.flatten end private @@ -81,19 +68,26 @@ module Gitlab def with_temp_file(storage_name) begin - temp_file_path = Dir::Tmpname.create(%w(fs_shards_check +deleted), path(storage_name)) { |path| path } + temp_file_path = Dir::Tmpname.create(%w(fs_shards_check +deleted), storage_path(storage_name)) { |path| path } yield temp_file_path ensure delete_test_file(temp_file_path) end end - def path(storage_name) + def storage_path(storage_name) storages_paths&.dig(storage_name, 'path') end + def delete_test_file(tmp_path) + _, status = exec_with_timeout(%W{ rm -f #{tmp_path} }) + status == 0 + rescue Errno::ENOENT + File.delete(tmp_path) rescue Errno::ENOENT + end + def storage_stat_test(storage_name) - stat_path = File.join(path(storage_name), '.') + stat_path = File.join(storage_path(storage_name), '.') begin _, status = exec_with_timeout(%W{ stat #{stat_path} }) status == 0 @@ -122,11 +116,27 @@ module Gitlab file_contents == RANDOM_STRING end - def delete_test_file(tmp_path) - _, status = exec_with_timeout(%W{ rm -f #{tmp_path} }) - status == 0 - rescue Errno::ENOENT - File.delete(tmp_path) rescue Errno::ENOENT + def storage_stat_metrics(storage_name) + operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, shard: storage_name) do + with_timing { storage_stat_test(storage_name) } + end + end + + def storage_write_metrics(storage_name) + operation_metrics(:filesystem_writable, :filesystem_write_latency_seconds, shard: storage_name) do + with_temp_file(storage_name) do |tmp_file_path| + with_timing { storage_write_test(tmp_file_path) } + end + end + end + + def storage_read_metrics(storage_name) + operation_metrics(:filesystem_readable, :filesystem_read_latency_seconds, shard: storage_name) do + with_temp_file(storage_name) do |tmp_file_path| + storage_write_test(tmp_file_path) # writes data used by read test + with_timing { storage_read_test(tmp_file_path) } + end + end end end end diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 947fb1b64d0..0a8dfa3bbdd 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -64,9 +64,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do it 'cleans up files used for testing' do expect(described_class).to receive(:storage_write_test).with(any_args).and_call_original - subject - - expect(Dir.entries(tmp_dir).count).to eq(2) + expect { subject }.not_to change(Dir.entries(tmp_dir), :count) end context 'read test fails' do @@ -88,8 +86,6 @@ describe Gitlab::HealthChecks::FsShardsCheck do end describe '#metrics' do - subject { described_class.metrics } - context 'storage points to not existing folder' do let(:storages_paths) do { @@ -104,14 +100,15 @@ describe Gitlab::HealthChecks::FsShardsCheck do end it 'provides metrics' do - expect(subject).to all(have_attributes(labels: { shard: :default })) - expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) + metrics = described_class.metrics - expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) + expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) end end @@ -121,21 +118,19 @@ describe Gitlab::HealthChecks::FsShardsCheck do end it 'provides metrics' do - expect(subject).to all(have_attributes(labels: { shard: :default })) + metrics = described_class.metrics - expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) - - expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) + expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) end it 'cleans up files used for metrics' do - subject - - expect(Dir.entries(tmp_dir).count).to eq(2) + expect { described_class.metrics }.not_to change(Dir.entries(tmp_dir), :count) end end end @@ -156,18 +151,16 @@ describe Gitlab::HealthChecks::FsShardsCheck do end describe '#metrics' do - subject { described_class.metrics } - it 'provides metrics' do - expect(subject).to all(have_attributes(labels: { shard: :default })) + metrics = described_class.metrics - expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) - - expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) - expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) + expect(metrics).to all(have_attributes(labels: { shard: :default })) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) end end end From acf4a36b3ed81c952d3f2edbfb054118b1d9dfff Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 21 Jul 2017 09:36:31 +0200 Subject: [PATCH 093/143] Implement GRPC call to RepositoryService --- app/models/repository.rb | 13 +++++++++--- lib/gitlab/git/repository.rb | 10 ++++++---- lib/gitlab/gitaly_client.rb | 2 +- .../gitaly_client/repository_service.rb | 16 +++++++++++++++ .../gitaly_client/repository_service_spec.rb | 19 ++++++++++++++++++ spec/models/repository_spec.rb | 20 +++++++++++-------- 6 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 lib/gitlab/gitaly_client/repository_service.rb create mode 100644 spec/lib/gitlab/gitaly_client/repository_service_spec.rb diff --git a/app/models/repository.rb b/app/models/repository.rb index 8663cf5e602..d27eeff9fb4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -471,8 +471,17 @@ class Repository end cache_method :root_ref + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/314 def exists? - refs_directory_exists? + return false unless path_with_namespace + + Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| + if enabled + raw_repository.exists? + else + refs_directory_exists? + end + end end cache_method :exists? @@ -1095,8 +1104,6 @@ class Repository end def refs_directory_exists? - return false unless path_with_namespace - File.exist?(File.join(path_to_repo, 'refs')) end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 63eebadff2e..3e27fd7b682 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -45,6 +45,8 @@ module Gitlab :bare?, to: :rugged + delegate :exists?, to: :gitaly_repository_client + # Default branch in the repository def root_ref @root_ref ||= gitaly_migrate(:root_ref) do |is_enabled| @@ -208,10 +210,6 @@ module Gitlab !empty? end - def repo_exists? - !!rugged - end - # Discovers the default branch based on the repository's available branches # # - If no branches are present, returns nil @@ -815,6 +813,10 @@ module Gitlab @gitaly_commit_client ||= Gitlab::GitalyClient::CommitService.new(self) end + def gitaly_repository_client + @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self) + end + private # Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'. diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 435e41e36fb..c90ef282fdd 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -57,7 +57,7 @@ module Gitlab metadata = yield(metadata) if block_given? stub(service, storage).send(rpc, request, metadata) end - + def self.request_metadata(storage) encoded_token = Base64.strict_encode64(token(storage).to_s) { metadata: { 'authorization' => "Bearer #{encoded_token}" } } diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb new file mode 100644 index 00000000000..f5d84ea8762 --- /dev/null +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -0,0 +1,16 @@ +module Gitlab + module GitalyClient + class RepositoryService + def initialize(repository) + @repository = repository + @gitaly_repo = repository.gitaly_repository + end + + def exists? + request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo) + + GitalyClient.call(@repository.storage, :repository_service, :exists, request).exists + end + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb new file mode 100644 index 00000000000..5a9f3fc130c --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::RepositoryService do + set(:project) { create(:empty_project) } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.path_with_namespace + '.git' } + let(:client) { described_class.new(project.repository) } + + describe '#exists?' do + it 'sends an exists message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:exists) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_call_original + + client.exists? + end + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7635b0868e7..fcda4248446 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -956,24 +956,28 @@ describe Repository, models: true do end end - describe '#exists?' do + shared_examples 'repo exists check' do it 'returns true when a repository exists' do expect(repository.exists?).to eq(true) end - it 'returns false when a repository does not exist' do - allow(repository).to receive(:refs_directory_exists?).and_return(false) - - expect(repository.exists?).to eq(false) - end - - it 'returns false when there is no namespace' do + it 'returns false if no full path can be constructed' do allow(repository).to receive(:path_with_namespace).and_return(nil) expect(repository.exists?).to eq(false) end end + describe '#exists?' do + context 'when repository_exists is disabled' do + it_behaves_like 'repo exists check' + end + + context 'when repository_exists is enabled', skip_gitaly_mock: true do + it_behaves_like 'repo exists check' + end + end + describe '#has_visible_content?' do subject { repository.has_visible_content? } From 2dc2538d740bcb1293808463b06f295522e8ce87 Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Wed, 26 Jul 2017 08:02:11 +0000 Subject: [PATCH 094/143] Docs new topic "user/index" --- doc/README.md | 7 +- doc/articles/index.md | 21 +++++ doc/integration/README.md | 24 +++--- doc/user/index.md | 175 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 doc/user/index.md diff --git a/doc/README.md b/doc/README.md index 2b3b0998fcc..ac7311a8c13 100644 --- a/doc/README.md +++ b/doc/README.md @@ -11,15 +11,11 @@ self-hosted, free to use. Every feature available in GitLab CE is also available self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)** and **GitLab Enterprise Edition Premium (EEP)**. - **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings). -**GitLab EE** contains all features available in **GitLab CE**, +> **GitLab EE** contains all features available in **GitLab CE**, plus premium features available in each version: **Enterprise Edition Starter** (**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in **EES** is also available in **EEP**. -**Note:** _We are unifying the documentation for CE and EE. To check if certain feature is -available in CE or EE, look for a note right below the page title containing the GitLab -version which introduced that feature._ - ---- Shortcuts to GitLab's most visited docs: @@ -40,6 +36,7 @@ Shortcuts to GitLab's most visited docs: ### User account +- [User documentation](user/index.md) - [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects. - [Profile settings](profile/README.md): Manage your profile settings, two factor authentication and more. - [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. diff --git a/doc/articles/index.md b/doc/articles/index.md index 342fa88e80f..a4e41517d83 100644 --- a/doc/articles/index.md +++ b/doc/articles/index.md @@ -11,6 +11,7 @@ They are written by members of the GitLab Team and by - **LDAP** - [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md) + - [How to configure LDAP with GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/) ## Git @@ -23,3 +24,23 @@ They are written by members of the GitLab Team and by - [Part 2: Quick start guide - Setting up GitLab Pages](../user/project/pages/getting_started_part_two.md) - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](../user/project/pages/getting_started_part_three.md) - [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](../user/project/pages/getting_started_part_four.md) +- [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) +- [GitLab CI: Deployment & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) + +## Sofware development + +- [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/2016/11/14/idea-to-production/) +- [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) +- [Fast and Natural Continuous Integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/) +- [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) +- [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) + +## Build, test, and deploy with GitLab CI/CD + +**Build, test, and deploy** the software you develop with **[GitLab CI/CD](../ci/README.md)** + +- [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) +- [Automated Debian Package Build with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/) +- [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/) +- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/) +- [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/) diff --git a/doc/integration/README.md b/doc/integration/README.md index e56e58498a6..d70b9a7f54b 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,19 +5,23 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker -- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. -- [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID -- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider -- [CAS](cas.md) Configure GitLab to sign in using CAS -- [OAuth2 provider](oauth_provider.md) OAuth2 application creation -- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider -- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages -- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users - [Akismet](akismet.md) Configure Akismet to stop spam +- [Auth0 OmniAuth](auth0.md) Enable the Auth0 OmniAuth provider +- [Bitbucket](bitbucket.md) Import projects from Bitbucket.org and login to your GitLab instance with your +Bitbucket.org account +- [CAS](cas.md) Configure GitLab to sign in using CAS +- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. +- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages +- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker - [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration +- [LDAP](ldap.md) Set up sign in via LDAP +- [OAuth2 provider](oauth_provider.md) OAuth2 application creation +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID +- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider - [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents. +- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users +- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [Trello](trello_power_up.md) Integrate Trello with GitLab > GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. diff --git a/doc/user/index.md b/doc/user/index.md new file mode 100644 index 00000000000..f545dbffde3 --- /dev/null +++ b/doc/user/index.md @@ -0,0 +1,175 @@ +# User documentation + +Welcome to GitLab! We're glad to have you here! + +As a GitLab user you'll have access to all the features +your [subscription](https://about.gitlab.com/products/) +includes, except [GitLab administrator](../README.md#administrator-documentation) +settings, unless you have admin privileges to install, configure, +and upgrade your GitLab instance. + +For GitLab.com, admin privileges are restricted to the GitLab team. + +If you run your own GitLab instance and are looking for the administration settings, +please refer to the [administration](../README.md#administrator-documentation) +documentation. + +## Overview + +GitLab is a fully integrated software development platform that enables you +and your team to work cohesively, faster, transparently, and effectively, +since the discussion of a new idea until taking that idea to production all +all the way through, from within the same platform. + +Please check this page for an overview on [GitLab's features](https://about.gitlab.com/features/). + +## Use cases + +GitLab is a git-based platforms that integrates a great number of essential tools for software development and deployment, and project management: + +- Code hosting in repositories with version control +- Track proposals for new implementations, bug reports, and feedback with a +fully featured [Issue Tracker](project/issues/index.md#issue-tracker) +- Organize and prioritize with [Issue Boards](project/issues/index.md#issue-boards) +- Code review in [Merge Requests](project/merge_requests/index.md) with live-preview changes per +branch with [Review Apps](../ci/review_apps/index.md) +- Build, test and deploy with built-in [Continuous Integration](../ci/README.md) +- Deploy your personal and professional static websites with [GitLab Pages](project/pages/index.md) +- Integrate with Docker with [GitLab Container Registry](project/container_registry.md) +- Track the development lifecycle with [GitLab Cycle Analytics](project/cycle_analytics.md) + +With GitLab Enterprise Edition, you can also: + +- Provide support with [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) +- Improve collaboration with +[Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#merge-request-approvals), +[Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html), +and [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards) +- Create formal relashionships between issues with [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html) +- Use [Burndown Charts](https://docs.gitlab.com/ee/user/project/milestones/burndown_charts.html) to track progress during a sprint or while working on a new version of their software. +- Leverage [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html) with [Advanced Global Search](https://docs.gitlab.com/ee/user/search/advanced_global_search.html) and [Advanced Syntax Search](https://docs.gitlab.com/ee/user/search/advanced_search_syntax.html) for faster, more advanced code search across your entire GitLab instance +- [Authenticate users with Kerberos](https://docs.gitlab.com/ee/integration/kerberos.html) +- [Mirror a repository](https://docs.gitlab.com/ee/workflow/repository_mirroring.html) from elsewhere on your local server. +- [Export issues as CSV](https://docs.gitlab.com/ee/user/project/issues/csv_export.html) +- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html) +- [Lock files](https://docs.gitlab.com/ee/user/project/file_lock.html) to prevent conflicts +- View of the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) +- Leverage your continuous delivery method with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) + +You can also [integrate](project/integrations/project_services.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, JIRA, and a lot more. + +### Articles + +For a complete workflow use case please check [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario). + +For more use cases please check our [Technical Articles](../articles/index.md). + +## Projects + +In GitLab, you can create projects for numerous reasons, such as, host +your code, use it as an issue tracker, collaborate on code, and continuously +build, test, and deploy your app with built-in GitLab CI/CD. Or, you can do +it all at once, from one single project. + +### Issues + +Explore the best of GitLab [Issues](project/issues/index.md). + +### Merge Requests + +Collanorate on code, gather reviews, live preview changes per branch, and +request approvals with [Merge Requests](project/merge_requests/index.md). + +### Milestones + +Work on multiple issues and merge requests towards the same target date +with [Milestones](project/milestones/index.md). + +### GitLab Pages + +Publish your static site directly from GitLab with [GitLab Pages](project/pages/index.md). You +can [build, test, and deploy any Static Site Generator](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) with Pages. + +### Container Registry + +Build and deploy Docker images with [GitLab Container Registry](project/container_registry.md). + +## GitLab CI/CD + +Use built-in [GitLab CI/CD](../ci/README.md) to test, build, and deploy your applications +directly from GitLab. No third-party integrations needed. + +### Auto Deploy + +Deploy your application out-of-the-box with [GitLab Auto Deploy](../ci/autodeploy/index.md). + +### Review Apps + +Live-preview the changes introduced by a merge request with [Review Apps](../ci/review_apps/index.md). + +## Groups + +With GitLab [Groups](group/index.md) you can assemble related projects together +and grant members access to several projects at once. + +### Subgroups + +Groups can also be nested in [subgroups](group/subgroups/index.md). + +## Account + +There is a lot you can customize and configure +to enjoy the best of GitLab. + +Manage your user settings to change your personal info, +personal access tokens, authorized applications, integrations, etc. + +### Authentication + +Read through the [authentication](../topics/authentication/index.md) methods available in GitLab. + +### Permissions + +Learn the different set of [permissions](permissions.md) for user type (guest, reporter, developer, master, owner). + +## Integrations + +[Integrate GitLab](../integration/README.md) with your preferred tool, +such as Trello, JIRA, etc. + +## Git and GitLab + +Learn what is [Git](../topics/git/index.md) and its best practices. + +## Discussions + +In GitLab, you can comment and mention collaborators in issues, +merge requests, code snippets, and commits. + +When performing inline reviews to implementations +to your codebase through merge requests you can +gather feedback through [resolvable discussions](discussions/index.md#resolvable-discussions). + +## Todos + +Never forget to reply to your collaborators. [GitLab Todos](../workflow/todos.md) +are a tool for working faster and more effectively with your team, +by listing all user or group mentions, as well as issues and merge +requests you're assigned to. + +## Snippets + +[Snippets](snippets.md) are code blocks that you want to store in GitLab, from which +you have quick access to. You can also gather feedback on them through +[discussions](#discussions). + +## Webhooks + +Configure [webhooks](project/integrations/webhooks.html) to listen for +specific events like pushes, issues or merge requests. GitLab will send a +POST request with data to the webhook URL. + +## API + +Automate GitLab via [API](../api/README.html). + From 29022350999ab3ddc4518f7a7647939ec2de8e09 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Fri, 16 Jun 2017 11:04:24 +1100 Subject: [PATCH 095/143] Add CSRF token verification to API --- ...601-add-csrf-token-verification-to-api.yml | 4 ++ lib/api/helpers.rb | 38 +++++++++++++++-- spec/lib/api/helpers/csrf_tokens_spec.rb | 42 +++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml create mode 100644 spec/lib/api/helpers/csrf_tokens_spec.rb diff --git a/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml b/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml new file mode 100644 index 00000000000..fa1a77ebcc1 --- /dev/null +++ b/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml @@ -0,0 +1,4 @@ +--- +title: Add CSRF token verification to API +merge_request: 12154 +author: @blackst0ne diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 57e3e93500f..ab5f4c865e0 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -328,6 +328,33 @@ module API private + def xor_byte_strings(s1, s2) + s2_bytes = s2.bytes + s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 } + s2_bytes.pack('C*') + end + + # Check if CSRF tokens are equal. + # The header token is masked. + # So, before the comparison it must be unmasked. + def csrf_tokens_valid?(request) + session_token = request.session['_csrf_token'] + header_token = request.headers['X-Csrf-Token'] + + session_token = Base64.strict_decode64(session_token) + header_token = Base64.strict_decode64(header_token) + + # Decoded CSRF token passed from the frontend has to be 64 symbols long. + return false if header_token.size != 64 + + header_token = xor_byte_strings(header_token[0...32], header_token[32..-1]) + + ActiveSupport::SecurityUtils.secure_compare(session_token, header_token) + + rescue + false + end + def private_token params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER] end @@ -336,12 +363,15 @@ module API env['warden'] end + def verified_request? + request = Grape::Request.new(env) + + request.head? || request.get? || csrf_tokens_valid?(request) + end + # Check the Rails session for valid authentication details - # - # Until CSRF protection is added to the API, disallow this method for - # state-changing endpoints def find_user_from_warden - warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD']) + warden.try(:authenticate) if verified_request? end def initial_current_user diff --git a/spec/lib/api/helpers/csrf_tokens_spec.rb b/spec/lib/api/helpers/csrf_tokens_spec.rb new file mode 100644 index 00000000000..d16db6c9064 --- /dev/null +++ b/spec/lib/api/helpers/csrf_tokens_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe API::Helpers do + subject do + Class.new.include(described_class).new + end + + let(:header_token) { 'WblCcheb1qQLHFVhlMtwOhxJr5613vUT05vCvToRvfJ68UPT7+eV5xpaY9CjubnF3VGbTfIhQYkZWmWTfvZAWQ==' } + let(:session_token) { 'I0gBofh8Q0MRRjaxN3LJ/8EYNNNH/7SaysGnLkTn/as=' } + + before do + class Request + attr_reader :headers + attr_reader :session + + def initialize(header_token = nil, session_token = nil) + @headers = { 'X-Csrf-Token' => header_token } + @session = { '_csrf_token' => session_token } + end + end + end + + it 'should return false if header token is invalid' do + request = Request.new(nil, session_token) + expect(subject.send(:csrf_tokens_valid?, request)).to be false + end + + it 'should return false if session_token token is invalid' do + request = Request.new(header_token, nil) + expect(subject.send(:csrf_tokens_valid?, request)).to be false + end + + it 'should return false if header_token is not 64 symbols long' do + request = Request.new(header_token[0..16], session_token) + expect(subject.send(:csrf_tokens_valid?, request)).to be false + end + + it 'should return true if both header_token and session_token are correct' do + request = Request.new(header_token, session_token) + expect(subject.send(:csrf_tokens_valid?, request)).to be true + end +end From 8ce8b21f675709c884148d050663b9f2374cdc61 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Wed, 21 Jun 2017 17:52:54 +1100 Subject: [PATCH 096/143] Refactor CSRF protection --- ...601-add-csrf-token-verification-to-api.yml | 2 +- config/initializers/omniauth.rb | 2 +- lib/api/helpers.rb | 32 ++----------------- .../request_forgery_protection.rb | 6 ++-- 4 files changed, 8 insertions(+), 34 deletions(-) rename lib/{omni_auth => gitlab}/request_forgery_protection.rb (66%) diff --git a/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml b/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml index fa1a77ebcc1..88cfb99a73e 100644 --- a/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml +++ b/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml @@ -1,4 +1,4 @@ --- title: Add CSRF token verification to API merge_request: 12154 -author: @blackst0ne +author: Vitaliy @blackst0ne Klachkov diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index f7fa6d1c2de..24ff3b924b5 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -16,7 +16,7 @@ OmniAuth.config.allowed_request_methods = [:post] # In case of auto sign-in, the GET method is used (users don't get to click on a button) OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? OmniAuth.config.before_request_phase do |env| - OmniAuth::RequestForgeryProtection.call(env) + GitLab::RequestForgeryProtection.call(env) end if Gitlab.config.omniauth.enabled diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index ab5f4c865e0..b81ce75ef4f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -328,33 +328,6 @@ module API private - def xor_byte_strings(s1, s2) - s2_bytes = s2.bytes - s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 } - s2_bytes.pack('C*') - end - - # Check if CSRF tokens are equal. - # The header token is masked. - # So, before the comparison it must be unmasked. - def csrf_tokens_valid?(request) - session_token = request.session['_csrf_token'] - header_token = request.headers['X-Csrf-Token'] - - session_token = Base64.strict_decode64(session_token) - header_token = Base64.strict_decode64(header_token) - - # Decoded CSRF token passed from the frontend has to be 64 symbols long. - return false if header_token.size != 64 - - header_token = xor_byte_strings(header_token[0...32], header_token[32..-1]) - - ActiveSupport::SecurityUtils.secure_compare(session_token, header_token) - - rescue - false - end - def private_token params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER] end @@ -363,10 +336,9 @@ module API env['warden'] end + # Check if CSRF tokens are valid. def verified_request? - request = Grape::Request.new(env) - - request.head? || request.get? || csrf_tokens_valid?(request) + GitLab::RequestForgeryProtection.call(env) end # Check the Rails session for valid authentication details diff --git a/lib/omni_auth/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb similarity index 66% rename from lib/omni_auth/request_forgery_protection.rb rename to lib/gitlab/request_forgery_protection.rb index 69155131d8d..071a72a1f8b 100644 --- a/lib/omni_auth/request_forgery_protection.rb +++ b/lib/gitlab/request_forgery_protection.rb @@ -1,6 +1,8 @@ -# Protects OmniAuth request phase against CSRF. +# A module to check CSRF tokens in requests. +# It's used in API helpers and OmniAuth. +# Usage: GitLab::RequestForgeryProtection.call(env) -module OmniAuth +module GitLab module RequestForgeryProtection class Controller < ActionController::Base protect_from_forgery with: :exception From cc3a82bc8bf0af9a8b7deb1b289aee621c91f7da Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Thu, 22 Jun 2017 16:19:14 +1100 Subject: [PATCH 097/143] Add `rescue false`. --- config/initializers/omniauth.rb | 2 +- lib/api/helpers.rb | 2 +- lib/gitlab/request_forgery_protection.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 24ff3b924b5..a36e59c941a 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -16,7 +16,7 @@ OmniAuth.config.allowed_request_methods = [:post] # In case of auto sign-in, the GET method is used (users don't get to click on a button) OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? OmniAuth.config.before_request_phase do |env| - GitLab::RequestForgeryProtection.call(env) + Gitlab::RequestForgeryProtection.call(env) end if Gitlab.config.omniauth.enabled diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index b81ce75ef4f..9a589828221 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -338,7 +338,7 @@ module API # Check if CSRF tokens are valid. def verified_request? - GitLab::RequestForgeryProtection.call(env) + Gitlab::RequestForgeryProtection.call(env) rescue false end # Check the Rails session for valid authentication details diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb index 071a72a1f8b..b0e15e2b655 100644 --- a/lib/gitlab/request_forgery_protection.rb +++ b/lib/gitlab/request_forgery_protection.rb @@ -2,7 +2,7 @@ # It's used in API helpers and OmniAuth. # Usage: GitLab::RequestForgeryProtection.call(env) -module GitLab +module Gitlab module RequestForgeryProtection class Controller < ActionController::Base protect_from_forgery with: :exception From 5a1f3df3b82361b613dbf718c4f7af26332297a1 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Thu, 22 Jun 2017 16:20:50 +1100 Subject: [PATCH 098/143] Remove spec/lib/api/helpers/csrf_tokens_spec.rb --- spec/lib/api/helpers/csrf_tokens_spec.rb | 42 ------------------------ 1 file changed, 42 deletions(-) delete mode 100644 spec/lib/api/helpers/csrf_tokens_spec.rb diff --git a/spec/lib/api/helpers/csrf_tokens_spec.rb b/spec/lib/api/helpers/csrf_tokens_spec.rb deleted file mode 100644 index d16db6c9064..00000000000 --- a/spec/lib/api/helpers/csrf_tokens_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' - -describe API::Helpers do - subject do - Class.new.include(described_class).new - end - - let(:header_token) { 'WblCcheb1qQLHFVhlMtwOhxJr5613vUT05vCvToRvfJ68UPT7+eV5xpaY9CjubnF3VGbTfIhQYkZWmWTfvZAWQ==' } - let(:session_token) { 'I0gBofh8Q0MRRjaxN3LJ/8EYNNNH/7SaysGnLkTn/as=' } - - before do - class Request - attr_reader :headers - attr_reader :session - - def initialize(header_token = nil, session_token = nil) - @headers = { 'X-Csrf-Token' => header_token } - @session = { '_csrf_token' => session_token } - end - end - end - - it 'should return false if header token is invalid' do - request = Request.new(nil, session_token) - expect(subject.send(:csrf_tokens_valid?, request)).to be false - end - - it 'should return false if session_token token is invalid' do - request = Request.new(header_token, nil) - expect(subject.send(:csrf_tokens_valid?, request)).to be false - end - - it 'should return false if header_token is not 64 symbols long' do - request = Request.new(header_token[0..16], session_token) - expect(subject.send(:csrf_tokens_valid?, request)).to be false - end - - it 'should return true if both header_token and session_token are correct' do - request = Request.new(header_token, session_token) - expect(subject.send(:csrf_tokens_valid?, request)).to be true - end -end From cb405aa45dd5acf766797a7375043b6608d394f8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 26 Jul 2017 11:19:57 +0200 Subject: [PATCH 099/143] Refactor max_size method in update pages service As per review feedback https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13072#note_35853177 --- app/services/projects/update_pages_service.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index a819b799ff8..749a1cc56d8 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -130,9 +130,11 @@ module Projects end def max_size - current_application_settings.max_pages_size.megabytes.tap do |maximum| - return MAX_SIZE if maximum.zero? || maximum > MAX_SIZE - end + max_pages_size = current_application_settings.max_pages_size.megabytes + + return MAX_SIZE if max_pages_size.zero? + + [max_pages_size, MAX_SIZE].min end def tmp_path From dcf4a2e83c69d1be0915f9c4c4f023abee2e7dea Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 26 Jul 2017 11:25:10 +0200 Subject: [PATCH 100/143] Rescue only from ActionController::InvalidAuthenticityToken --- lib/api/helpers.rb | 4 ++-- lib/gitlab/request_forgery_protection.rb | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 9a589828221..234825480f2 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -336,9 +336,9 @@ module API env['warden'] end - # Check if CSRF tokens are valid. + # Check if the request is GET/HEAD, or if CSRF token is valid. def verified_request? - Gitlab::RequestForgeryProtection.call(env) rescue false + Gitlab::RequestForgeryProtection.verified?(env) end # Check the Rails session for valid authentication details diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb index b0e15e2b655..48dd0487790 100644 --- a/lib/gitlab/request_forgery_protection.rb +++ b/lib/gitlab/request_forgery_protection.rb @@ -19,5 +19,13 @@ module Gitlab def self.call(env) app.call(env) end + + def self.verified?(env) + call(env) + + true + rescue ActionController::InvalidAuthenticityToken + false + end end end From 9aa2205a15c72394234892ef3babe94ce7eb1828 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Wed, 26 Jul 2017 09:31:17 +0000 Subject: [PATCH 101/143] Resolve "Memory usage notice doesn't link anywhere" --- .../components/mr_widget_deployment.js | 3 ++- .../components/mr_widget_memory_usage.js | 11 +++++++++-- app/controllers/projects/merge_requests_controller.rb | 6 ++++++ ...4110-memory-usage-notice-doesn-t-link-anywhere.yml | 4 ++++ .../components/mr_widget_deployment_spec.js | 1 + .../components/mr_widget_memory_usage_spec.js | 2 ++ 6 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index e8e22ad93a5..744a1cd24fa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -108,7 +108,8 @@ export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 76cb71b6c12..534e2a88eff 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -7,7 +7,14 @@ import MRWidgetService from '../services/mr_widget_service'; export default { name: 'MemoryUsage', props: { - metricsUrl: { type: String, required: true }, + metricsUrl: { + type: String, + required: true, + }, + metricsMonitoringUrl: { + type: String, + required: true, + }, }, data() { return { @@ -124,7 +131,7 @@ export default {

- Memory usage {{memoryChangeType}} from {{memoryFrom}}MB to {{memoryTo}}MB + Memory usage {{memoryChangeType}} from {{memoryFrom}}MB to {{memoryTo}}MB

{ el: document.createElement('div'), propsData: { metricsUrl: url, + metricsMonitoringUrl: monitoringUrl, memoryMetrics: [], deploymentTime: 0, hasMetrics: false, From 8ab29d569e8c0019bbe458dea6f05a9894f0711a Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 08:27:39 -0700 Subject: [PATCH 102/143] Upgrade omniauth-ldap gem (and some dependencies) Most notably, the net-ldap gem. --- Gemfile | 2 +- Gemfile.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index 7c7122a39ea..276893d0ff4 100644 --- a/Gemfile +++ b/Gemfile @@ -61,7 +61,7 @@ gem 'browser', '~> 2.2' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master -gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap' +gem 'gitlab_omniauth-ldap', '~> 2.0.1', require: 'omniauth-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order diff --git a/Gemfile.lock b/Gemfile.lock index 901e5334994..2b2df1b6bcc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -288,11 +288,11 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.1) - gitlab_omniauth-ldap (1.2.1) - net-ldap (~> 0.9) - omniauth (~> 1.0) - pyu-ruby-sasl (~> 0.0.3.1) - rubyntlm (~> 0.3) + gitlab_omniauth-ldap (2.0.1) + net-ldap (~> 0.16) + omniauth (~> 1.3) + pyu-ruby-sasl (>= 0.0.3.3, < 0.1) + rubyntlm (~> 0.5) globalid (0.3.7) activesupport (>= 4.1.0) gollum-grit_adapter (1.0.1) @@ -471,7 +471,7 @@ GEM mustermann-grape (1.0.0) mustermann (~> 1.0.0) mysql2 (0.4.5) - net-ldap (0.12.1) + net-ldap (0.16.0) netrc (0.11.0) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) @@ -744,7 +744,7 @@ GEM nokogiri (>= 1.5.10) ruby_parser (3.9.0) sexp_processor (~> 4.1) - rubyntlm (0.5.2) + rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.1) rufus-scheduler (3.4.0) @@ -978,7 +978,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) - gitlab_omniauth-ldap (~> 1.2.1) + gitlab_omniauth-ldap (~> 2.0.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.4) gon (~> 6.1.0) From 6dbff9663de072279bd027e8e3e453b732f75977 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 6 Jun 2017 11:16:17 -0700 Subject: [PATCH 103/143] Add LDAP config options --- config/gitlab.yml.example | 30 +++++++++++++++++++++++++++++- config/initializers/1_settings.rb | 5 +++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cb007813b65..8ddd9bab4e6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -254,10 +254,38 @@ production: &base host: '_your_ldap_server' port: 389 uid: 'sAMAccountName' - method: 'plain' # "tls" or "ssl" or "plain" bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' + # Encryption method. The "method" key is deprecated in favor of + # "encryption". + # + # Examples: "start_tls" or "simple_tls" or "plain" + # + # Deprecated values: "tls" was replaced with "start_tls" and "ssl" was + # replaced with "simple_tls". + # + encryption: 'plain' + + # Enables SSL certificate verification if encryption method is + # "start_tls" or "simple_tls". (Defaults to false for backward- + # compatibility) + verify_certificates: false + + # Specifies the path to a file containing a PEM-format CA certificate, + # e.g. if you need to use an internal CA. + # + # Example: '/etc/ca.pem' + # + ca_cert: '' + + # Specifies the SSL version for OpenSSL to use, if the OpenSSL default + # is not appropriate. + # + # Example: 'TLSv1_1' + # + ssl_version: '' + # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking # a request if the LDAP server becomes unresponsive. # A value of 0 means there is no timeout. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index ec7ce51b542..9344a42540b 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -145,6 +145,11 @@ if Settings.ldap['enabled'] || Rails.env.test? server['attributes'] = {} if server['attributes'].nil? server['provider_name'] ||= "ldap#{key}".downcase server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name']) + server['encryption'] ||= server['method'] # for backwards compatibility + + # Certificates are not verified for backwards compatibility. + # This default should be flipped to true in 9.5. + server['verify_certificates'] = false if server['verify_certificates'].nil? end end From 94b4c9f34f576bbeddc2a22098f33c6ae656d7ab Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 8 Jun 2017 15:46:06 -0700 Subject: [PATCH 104/143] Use encryption instead of method The method key is deprecated in the `gitlab_omniauth-ldap` gem. --- lib/gitlab/ldap/config.rb | 4 ++-- spec/lib/gitlab/ldap/config_spec.rb | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 6fdf68641e2..c531100fbc4 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -50,7 +50,7 @@ module Gitlab def omniauth_options opts = base_options.merge( base: base, - method: options['method'], + encryption: options['encryption'], filter: omniauth_user_filter, name_proc: name_proc ) @@ -158,7 +158,7 @@ module Gitlab end def encryption - case options['method'].to_s + case options['encryption'].to_s when 'ssl' :simple_tls when 'tls' diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index cab2e9908ff..e75e1e3ea2f 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -23,9 +23,9 @@ describe Gitlab::LDAP::Config, lib: true do it 'constructs basic options' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 386, - 'method' => 'plain' + 'host' => 'ldap.example.com', + 'port' => 386, + 'encryption' => 'plain' } ) @@ -39,11 +39,11 @@ describe Gitlab::LDAP::Config, lib: true do it 'includes authentication options when auth is configured' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'method' => 'ssl', - 'bind_dn' => 'uid=admin,dc=example,dc=com', - 'password' => 'super_secret' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'ssl', + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' } ) @@ -64,11 +64,11 @@ describe Gitlab::LDAP::Config, lib: true do it 'constructs basic options' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 386, - 'base' => 'ou=users,dc=example,dc=com', - 'method' => 'plain', - 'uid' => 'uid' + 'host' => 'ldap.example.com', + 'port' => 386, + 'base' => 'ou=users,dc=example,dc=com', + 'encryption' => 'plain', + 'uid' => 'uid' } ) @@ -76,7 +76,7 @@ describe Gitlab::LDAP::Config, lib: true do host: 'ldap.example.com', port: 386, base: 'ou=users,dc=example,dc=com', - method: 'plain', + encryption: 'plain', filter: '(uid=%{username})' ) expect(config.omniauth_options.keys).not_to include(:bind_dn, :password) From b67c007842ba42d2ed1cf1d8879a220a1b9906f9 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 8 Jun 2017 16:30:54 -0700 Subject: [PATCH 105/143] Set `Net::LDAP` encryption properly --- lib/gitlab/ldap/config.rb | 34 +++++++--- spec/lib/gitlab/ldap/config_spec.rb | 102 +++++++++++++++++++++++++--- 2 files changed, 117 insertions(+), 19 deletions(-) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index c531100fbc4..383e0a09e42 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -2,6 +2,16 @@ module Gitlab module LDAP class Config + NET_LDAP_ENCRYPTION_METHOD = { + :simple_tls => :simple_tls, + :start_tls => :start_tls, + :plain => nil, + + # Deprecated. Better to pass-through the actual `Net::LDAP` encryption type. + :ssl => :simple_tls, + :tls => :start_tls, + } + attr_accessor :provider, :options def self.enabled? @@ -39,7 +49,7 @@ module Gitlab def adapter_options opts = base_options.merge( - encryption: encryption + encryption: encryption_options ) opts.merge!(auth_options) if has_auth? @@ -157,14 +167,22 @@ module Gitlab base_config.servers.values.find { |server| server['provider_name'] == provider } end - def encryption - case options['encryption'].to_s - when 'ssl' - :simple_tls - when 'tls' - :start_tls + def encryption_options + method = translate_method(options['encryption']) + options = { method: method } + options.merge!(tls_options: tls_options(method)) if method + options + end + + def translate_method(method_from_config) + NET_LDAP_ENCRYPTION_METHOD[method_from_config.to_sym] + end + + def tls_options(method) + if method && options['verify_certificates'] + OpenSSL::SSL::SSLContext::DEFAULT_PARAMS else - nil + { verify_mode: OpenSSL::SSL::VERIFY_NONE } end end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index e75e1e3ea2f..bbd4da58252 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::LDAP::Config, lib: true do let(:config) { Gitlab::LDAP::Config.new('ldapmain') } - describe '#initalize' do + describe '#initialize' do it 'requires a provider' do expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError end @@ -32,31 +32,111 @@ describe Gitlab::LDAP::Config, lib: true do expect(config.adapter_options).to eq( host: 'ldap.example.com', port: 386, - encryption: nil + encryption: { method: nil } ) end it 'includes authentication options when auth is configured' do stub_ldap_config( options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'ssl', - 'bind_dn' => 'uid=admin,dc=example,dc=com', - 'password' => 'super_secret' + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => true, + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' } ) - expect(config.adapter_options).to eq( - host: 'ldap.example.com', - port: 686, - encryption: :simple_tls, + expect(config.adapter_options).to include({ auth: { method: :simple, username: 'uid=admin,dc=example,dc=com', password: 'super_secret' } + }) + end + + it 'sets encryption method to simple_tls when configured as simple_tls' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls' + } ) + + expect(config.adapter_options[:encryption]).to include({ method: :simple_tls }) + end + + it 'sets encryption method to simple_tls when configured as ssl, for backwards compatibility' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'ssl' + } + ) + + expect(config.adapter_options[:encryption]).to include({ method: :simple_tls }) + end + + it 'sets encryption method to start_tls when configured as start_tls' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls' + } + ) + + expect(config.adapter_options[:encryption]).to include({ method: :start_tls }) + end + + it 'sets encryption method to start_tls when configured as tls, for backwards compatibility' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'tls' + } + ) + + expect(config.adapter_options[:encryption]).to include({ method: :start_tls }) + end + + context 'when verify_certificates is enabled' do + it 'sets tls_options to OpenSSL defaults' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => true + } + ) + + expect(config.adapter_options[:encryption]).to include({ tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS }) + end + end + + context 'when verify_certificates is disabled' do + it 'sets verify_mode to OpenSSL VERIFY_NONE' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => false + } + ) + + expect(config.adapter_options[:encryption]).to include({ + tls_options: { + verify_mode: OpenSSL::SSL::VERIFY_NONE + } + }) + end end end From dcc12505aa121f809f6cf64fa7a68cc5457aca31 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 8 Jun 2017 16:57:13 -0700 Subject: [PATCH 106/143] Set `Net::LDAP` `ca_file` option --- lib/gitlab/ldap/config.rb | 20 ++++++++++++++----- spec/lib/gitlab/ldap/config_spec.rb | 30 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 383e0a09e42..983c79a6364 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -179,11 +179,21 @@ module Gitlab end def tls_options(method) - if method && options['verify_certificates'] - OpenSSL::SSL::SSLContext::DEFAULT_PARAMS - else - { verify_mode: OpenSSL::SSL::VERIFY_NONE } - end + return { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless method + + opts = if options['verify_certificates'] + OpenSSL::SSL::SSLContext::DEFAULT_PARAMS + else + # It is important to explicitly set verify_mode for two reasons: + # 1. The behavior of OpenSSL is undefined when verify_mode is not set. + # 2. The net-ldap gem implementation verifies the certificate hostname + # unless verify_mode is set to VERIFY_NONE. + { verify_mode: OpenSSL::SSL::VERIFY_NONE } + end + + opts[:ca_file] = options['ca_file'] if options['ca_file'].present? + + opts end def auth_options diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index bbd4da58252..4544a38876c 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -138,6 +138,36 @@ describe Gitlab::LDAP::Config, lib: true do }) end end + + context 'when ca_file is specified' do + it 'passes it through in tls_options' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'ca_file' => '/etc/ca.pem' + } + ) + + expect(config.adapter_options[:encryption][:tls_options]).to include({ ca_file: '/etc/ca.pem' }) + end + end + + context 'when ca_file is a blank string' do + it 'does not add the ca_file key to tls_options' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'ca_file' => ' ' + } + ) + + expect(config.adapter_options[:encryption][:tls_options]).not_to have_key(:ca_file) + end + end end describe '#omniauth_options' do From 612b3864505a9e7445d09a80efa263cca9d8758d Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 8 Jun 2017 17:03:57 -0700 Subject: [PATCH 107/143] Set `Net::LDAP` `ssl_version` option --- lib/gitlab/ldap/config.rb | 1 + spec/lib/gitlab/ldap/config_spec.rb | 30 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 983c79a6364..a48a485dffd 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -192,6 +192,7 @@ module Gitlab end opts[:ca_file] = options['ca_file'] if options['ca_file'].present? + opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present? opts end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 4544a38876c..e24c7d6b9a2 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -168,6 +168,36 @@ describe Gitlab::LDAP::Config, lib: true do expect(config.adapter_options[:encryption][:tls_options]).not_to have_key(:ca_file) end end + + context 'when ssl_version is specified' do + it 'passes it through in tls_options' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'ssl_version' => 'TLSv1_2' + } + ) + + expect(config.adapter_options[:encryption][:tls_options]).to include({ ssl_version: 'TLSv1_2' }) + end + end + + context 'when ssl_version is a blank string' do + it 'does not add the ssl_version key to tls_options' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'ssl_version' => ' ' + } + ) + + expect(config.adapter_options[:encryption][:tls_options]).not_to have_key(:ssl_version) + end + end end describe '#omniauth_options' do From cd13e4ae734f6a5ff2d02986138bda54267425ae Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 10:29:19 -0700 Subject: [PATCH 108/143] Verify certificates in `omniauth-ldap` --- lib/gitlab/ldap/config.rb | 3 ++- spec/lib/gitlab/ldap/config_spec.rb | 30 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index a48a485dffd..9ed88330900 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -62,7 +62,8 @@ module Gitlab base: base, encryption: options['encryption'], filter: omniauth_user_filter, - name_proc: name_proc + name_proc: name_proc, + disable_verify_certificates: !options['verify_certificates'] ) if has_auth? diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index e24c7d6b9a2..0cebbab5c24 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -238,6 +238,36 @@ describe Gitlab::LDAP::Config, lib: true do password: 'super_secret' ) end + + context 'when verify_certificates is enabled' do + it 'specifies disable_verify_certificates as false' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => true + } + ) + + expect(config.omniauth_options).to include({ disable_verify_certificates: false }) + end + end + + context 'when verify_certificates is disabled' do + it 'specifies disable_verify_certificates as true' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => false + } + ) + + expect(config.omniauth_options).to include({ disable_verify_certificates: true }) + end + end end describe '#has_auth?' do From c8dd77de81f42c593dcbf0b373afd0ab33f18071 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 10:30:17 -0700 Subject: [PATCH 109/143] Pass configured `ca_file` to `omniauth-ldap` --- lib/gitlab/ldap/config.rb | 1 + spec/lib/gitlab/ldap/config_spec.rb | 33 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 9ed88330900..3f88e20bbec 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -73,6 +73,7 @@ module Gitlab ) end + opts[:ca_file] = options['ca_file'] if options['ca_file'].present? opts end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 0cebbab5c24..107084519f9 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -268,6 +268,39 @@ describe Gitlab::LDAP::Config, lib: true do expect(config.omniauth_options).to include({ disable_verify_certificates: true }) end end + + context 'when ca_file is present' do + it 'passes it through' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => true, + 'ca_file' => '/etc/ca.pem' + } + ) + + expect(config.omniauth_options).to include({ ca_file: '/etc/ca.pem' }) + end + end + + context 'when ca_file is blank' do + it 'does not include the ca_file option' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => true, + 'ca_file' => ' ' + } + ) + + expect(config.omniauth_options).not_to have_key(:ca_file) + end + end + end describe '#has_auth?' do From 2d7d1fa69db2b5e0056d5ab8884684886229f852 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 10:30:38 -0700 Subject: [PATCH 110/143] Pass configured `ssl_version` to `omniauth-ldap` --- lib/gitlab/ldap/config.rb | 2 ++ spec/lib/gitlab/ldap/config_spec.rb | 31 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 3f88e20bbec..efc3c50e038 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -74,6 +74,8 @@ module Gitlab end opts[:ca_file] = options['ca_file'] if options['ca_file'].present? + opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present? + opts end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 107084519f9..7679c9ea913 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -301,6 +301,37 @@ describe Gitlab::LDAP::Config, lib: true do end end + context 'when ssl_version is present' do + it 'passes it through' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => true, + 'ssl_version' => 'TLSv1_2' + } + ) + + expect(config.omniauth_options).to include({ ssl_version: 'TLSv1_2' }) + end + end + + context 'when ssl_version is blank' do + it 'does not include the ssl_version option' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'simple_tls', + 'verify_certificates' => true, + 'ssl_version' => ' ' + } + ) + + expect(config.omniauth_options).not_to have_key(:ssl_version) + end + end end describe '#has_auth?' do From 72d8b1e40aa96f575aac9a8c9dada09e66cd7a9d Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 10:39:29 -0700 Subject: [PATCH 111/143] Move backwards compatibility logic out of the code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And closer to the configuration setup. The code doesn’t need to know about this. --- config/initializers/1_settings.rb | 6 +++++- lib/gitlab/ldap/config.rb | 6 +----- spec/lib/gitlab/ldap/config_spec.rb | 24 ------------------------ 3 files changed, 6 insertions(+), 30 deletions(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 9344a42540b..20fe92dd6b3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -145,7 +145,11 @@ if Settings.ldap['enabled'] || Rails.env.test? server['attributes'] = {} if server['attributes'].nil? server['provider_name'] ||= "ldap#{key}".downcase server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name']) - server['encryption'] ||= server['method'] # for backwards compatibility + + # For backwards compatibility + server['encryption'] ||= server['method'] + server['encryption'] = 'simple_tls' if server['encryption'] == 'ssl' + server['encryption'] = 'start_tls' if server['encryption'] == 'tls' # Certificates are not verified for backwards compatibility. # This default should be flipped to true in 9.5. diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index efc3c50e038..db76ee098c5 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -5,11 +5,7 @@ module Gitlab NET_LDAP_ENCRYPTION_METHOD = { :simple_tls => :simple_tls, :start_tls => :start_tls, - :plain => nil, - - # Deprecated. Better to pass-through the actual `Net::LDAP` encryption type. - :ssl => :simple_tls, - :tls => :start_tls, + :plain => nil } attr_accessor :provider, :options diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 7679c9ea913..e3a9505531d 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -69,18 +69,6 @@ describe Gitlab::LDAP::Config, lib: true do expect(config.adapter_options[:encryption]).to include({ method: :simple_tls }) end - it 'sets encryption method to simple_tls when configured as ssl, for backwards compatibility' do - stub_ldap_config( - options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'ssl' - } - ) - - expect(config.adapter_options[:encryption]).to include({ method: :simple_tls }) - end - it 'sets encryption method to start_tls when configured as start_tls' do stub_ldap_config( options: { @@ -93,18 +81,6 @@ describe Gitlab::LDAP::Config, lib: true do expect(config.adapter_options[:encryption]).to include({ method: :start_tls }) end - it 'sets encryption method to start_tls when configured as tls, for backwards compatibility' do - stub_ldap_config( - options: { - 'host' => 'ldap.example.com', - 'port' => 686, - 'encryption' => 'tls' - } - ) - - expect(config.adapter_options[:encryption]).to include({ method: :start_tls }) - end - context 'when verify_certificates is enabled' do it 'sets tls_options to OpenSSL defaults' do stub_ldap_config( From 71c36c5bb48ad70ec6f079bbedd6114b769805fa Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 11:43:07 -0700 Subject: [PATCH 112/143] Add warning about certificate verification on load --- config/initializers/1_settings.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 20fe92dd6b3..201a1d062b9 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -153,7 +153,16 @@ if Settings.ldap['enabled'] || Rails.env.test? # Certificates are not verified for backwards compatibility. # This default should be flipped to true in 9.5. - server['verify_certificates'] = false if server['verify_certificates'].nil? + if server['verify_certificates'].nil? + server['verify_certificates'] = false + + message = <<-MSG.strip_heredoc + LDAP SSL certificate verification is disabled for backwards-compatibility. + Please add the "verify_certificates" option to gitlab.yml for each LDAP + server. Certificate verification will be enabled by default in GitLab 9.5. + MSG + Rails.logger.warn(message) + end end end From 0b4eb7f21851b478d7fe179a1213d090d8ce4c57 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 11:47:20 -0700 Subject: [PATCH 113/143] Fix code style --- lib/gitlab/ldap/config.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index db76ee098c5..163e49ecc1c 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -3,10 +3,10 @@ module Gitlab module LDAP class Config NET_LDAP_ENCRYPTION_METHOD = { - :simple_tls => :simple_tls, - :start_tls => :start_tls, - :plain => nil - } + simple_tls: :simple_tls, + start_tls: :start_tls, + plain: nil + }.freeze attr_accessor :provider, :options @@ -170,7 +170,7 @@ module Gitlab def encryption_options method = translate_method(options['encryption']) options = { method: method } - options.merge!(tls_options: tls_options(method)) if method + options[:tls_options] = tls_options(method) if method options end From fdaa49ca29c458a99cdae207784ecf10f0d208c0 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 15:35:41 -0700 Subject: [PATCH 114/143] Update LDAP SSL config options --- doc/administration/auth/ldap.md | 36 +++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 3449f9e15ce..90dd9d6a51b 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -69,14 +69,42 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server # Example: 'ldap.mydomain.com' host: '_your_ldap_server' # This port is an example, it is sometimes different but it is always an integer and not a string - port: 389 + port: 389 # usually 636 for SSL uid: 'sAMAccountName' # This should be the attribute, not the value that maps to uid. - method: 'plain' # "tls" or "ssl" or "plain" # Examples: 'america\\momo' or 'CN=Gitlab Git,CN=Users,DC=mydomain,DC=com' bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' + # Encryption method. The "method" key is deprecated in favor of + # "encryption". + # + # Examples: "start_tls" or "simple_tls" or "plain" + # + # Deprecated values: "tls" was replaced with "start_tls" and "ssl" was + # replaced with "simple_tls". + # + encryption: 'plain' + + # Enables SSL certificate verification if encryption method is + # "start_tls" or "simple_tls". (Defaults to false for backward- + # compatibility) + verify_certificates: false + + # Specifies the path to a file containing a PEM-format CA certificate, + # e.g. if you need to use an internal CA. + # + # Example: '/etc/ca.pem' + # + ca_cert: '' + + # Specifies the SSL version for OpenSSL to use, if the OpenSSL default + # is not appropriate. + # + # Example: 'TLSv1_1' + # + ssl_version: '' + # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking # a request if the LDAP server becomes unresponsive. # A value of 0 means there is no timeout. @@ -116,8 +144,8 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server # # Note: GitLab does not support omniauth-ldap's custom filter syntax. # - # Below an example for get only specific users - # Example: '(&(objectclass=user)(|(samaccountname=momo)(samaccountname=toto)))' + # Example for getting only specific users: + # '(&(objectclass=user)(|(samaccountname=momo)(samaccountname=toto)))' # user_filter: '' From e0fe34778d4705111db0b553925be7da5ee3c66d Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 15:37:23 -0700 Subject: [PATCH 115/143] Copy comment improvements from documentation --- config/gitlab.yml.example | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 8ddd9bab4e6..f702e812955 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -251,9 +251,13 @@ production: &base # Example: 'Paris' or 'Acme, Ltd.' label: 'LDAP' + # Example: 'ldap.mydomain.com' host: '_your_ldap_server' - port: 389 - uid: 'sAMAccountName' + # This port is an example, it is sometimes different but it is always an integer and not a string + port: 389 # usually 636 for SSL + uid: 'sAMAccountName' # This should be the attribute, not the value that maps to uid. + + # Examples: 'america\\momo' or 'CN=Gitlab Git,CN=Users,DC=mydomain,DC=com' bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' @@ -314,17 +318,20 @@ production: &base # Base where we can search for users # - # Ex. ou=People,dc=gitlab,dc=example + # Ex. 'ou=People,dc=gitlab,dc=example' or 'DC=mydomain,DC=com' # base: '' # Filter LDAP users # - # Format: RFC 4515 http://tools.ietf.org/search/rfc4515 + # Format: RFC 4515 https://tools.ietf.org/search/rfc4515 # Ex. (employeeType=developer) # # Note: GitLab does not support omniauth-ldap's custom filter syntax. # + # Example for getting only specific users: + # '(&(objectclass=user)(|(samaccountname=momo)(samaccountname=toto)))' + # user_filter: '' # LDAP attributes that GitLab will use to create an account for the LDAP user. From 857dcd6c76d5219aecb15c391b3ee7994f1dbb25 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 16:01:59 -0700 Subject: [PATCH 116/143] Change encryption description --- doc/administration/auth/ldap.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 90dd9d6a51b..a7395e03d1c 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -278,6 +278,19 @@ In other words, if an existing GitLab user wants to enable LDAP sign-in for themselves, they should check that their GitLab email address matches their LDAP email address, and then sign into GitLab via their LDAP credentials. +## Encryption + +### TLS Server Authentication + +There are two encryption methods, `simple_tls` and `start_tls`. + +For either encryption method, if setting `validate_certificates: false`, TLS +encryption is established with the LDAP server before any LDAP-protocol data is +exchanged but no validation of the LDAP server's SSL certificate is performed. + +>**Note**: Before GitLab 9.5, `validate_certificates: false` is the default if +unspecified. + ## Limitations ### TLS Client Authentication @@ -287,14 +300,6 @@ You should disable anonymous LDAP authentication and enable simple or SASL authentication. The TLS client authentication setting in your LDAP server cannot be mandatory and clients cannot be authenticated with the TLS protocol. -### TLS Server Authentication - -Not supported by GitLab's configuration options. -When setting `method: ssl`, the underlying authentication method used by -`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with -the LDAP server before any LDAP-protocol data is exchanged but no validation of -the LDAP server's SSL certificate is performed. - ## Troubleshooting ### Debug LDAP user filter with ldapsearch @@ -334,9 +339,9 @@ tree and traverse it. ### Connection Refused If you are getting 'Connection Refused' errors when trying to connect to the -LDAP server please double-check the LDAP `port` and `method` settings used by -GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR -`method: 'ssl'` and `port: 636`. +LDAP server please double-check the LDAP `port` and `encryption` settings used by +GitLab. Common combinations are `encryption: 'plain'` and `port: 389`, OR +`encryption: 'simple_tls'` and `port: 636`. ### Troubleshooting From 8bc20a8e00111a1453e51d88fcf7520091b47d02 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 9 Jun 2017 16:09:52 -0700 Subject: [PATCH 117/143] Add changelog entry --- .../unreleased/mk-add-ldap-ssl-certificate-verification.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/mk-add-ldap-ssl-certificate-verification.yml diff --git a/changelogs/unreleased/mk-add-ldap-ssl-certificate-verification.yml b/changelogs/unreleased/mk-add-ldap-ssl-certificate-verification.yml new file mode 100644 index 00000000000..80e6c50d5b3 --- /dev/null +++ b/changelogs/unreleased/mk-add-ldap-ssl-certificate-verification.yml @@ -0,0 +1,4 @@ +--- +title: Add LDAP SSL certificate verification option +merge_request: +author: From 951bd2a43161206dbafe2a6099098e5fb48a6005 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 12 Jun 2017 10:17:49 -0700 Subject: [PATCH 118/143] Update more examples --- config/gitlab.yml.example | 2 +- doc/articles/how_to_configure_ldap_gitlab_ce/index.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index f702e812955..106658ad12b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -709,7 +709,7 @@ test: host: 127.0.0.1 port: 3890 uid: 'uid' - method: 'plain' # "tls" or "ssl" or "plain" + encryption: 'plain' # "start_tls" or "simple_tls" or "plain" base: 'dc=example,dc=com' user_filter: '' group_base: 'ou=groups,dc=example,dc=com' diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md index 6892905dd94..130e8f542b4 100644 --- a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md +++ b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md @@ -120,7 +120,8 @@ gitlab_rails['ldap_servers'] = { 'host' => 'ad.example.org', 'port' => 636, 'uid' => 'sAMAccountName', - 'method' => 'ssl', + 'encryption' => 'simple_tls', + 'verify_certificates' => true, 'bind_dn' => 'CN=GitLabSRV,CN=Users,DC=GitLab,DC=org', 'password' => 'Password1', 'active_directory' => true, @@ -255,7 +256,7 @@ If `allow_username_or_email_login` is enabled in the LDAP configuration, GitLab ## LDAP extended features on GitLab EE -With [GitLab Enterprise Edition (EE)](https://about.gitlab.com/giltab-ee/), besides everything we just described, you'll +With [GitLab Enterprise Edition (EE)](https://about.gitlab.com/gitlab-ee/), besides everything we just described, you'll have extended functionalities with LDAP, such as: - Group sync From 7f92a36a36ab8183c843982bf91bdabb45861154 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 13 Jun 2017 14:01:28 -0700 Subject: [PATCH 119/143] Fix plain LDAP (no encryption) --- Gemfile | 2 +- Gemfile.lock | 4 ++-- lib/gitlab/ldap/config.rb | 9 ++++++--- spec/lib/gitlab/ldap/config_spec.rb | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 276893d0ff4..7bb912571f4 100644 --- a/Gemfile +++ b/Gemfile @@ -61,7 +61,7 @@ gem 'browser', '~> 2.2' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master -gem 'gitlab_omniauth-ldap', '~> 2.0.1', require: 'omniauth-ldap' +gem 'gitlab_omniauth-ldap', '~> 2.0.2', require: 'omniauth-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order diff --git a/Gemfile.lock b/Gemfile.lock index 2b2df1b6bcc..661f0294646 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -288,7 +288,7 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.1) - gitlab_omniauth-ldap (2.0.1) + gitlab_omniauth-ldap (2.0.2) net-ldap (~> 0.16) omniauth (~> 1.3) pyu-ruby-sasl (>= 0.0.3.3, < 0.1) @@ -978,7 +978,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) - gitlab_omniauth-ldap (~> 2.0.1) + gitlab_omniauth-ldap (~> 2.0.2) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.4) gon (~> 6.1.0) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 163e49ecc1c..8eda3ea03f9 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -169,9 +169,12 @@ module Gitlab def encryption_options method = translate_method(options['encryption']) - options = { method: method } - options[:tls_options] = tls_options(method) if method - options + return nil unless method + + { + method: method, + tls_options: tls_options(method) + } end def translate_method(method_from_config) diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index e3a9505531d..3a56797d68b 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::LDAP::Config, lib: true do expect(config.adapter_options).to eq( host: 'ldap.example.com', port: 386, - encryption: { method: nil } + encryption: nil ) end From 26ee3a28029597f6813e652a09288504e9a2291d Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 13 Jun 2017 14:03:25 -0700 Subject: [PATCH 120/143] Mention how to test LDAP connections --- config/gitlab.yml.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 106658ad12b..e9bf2df490f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -228,7 +228,8 @@ production: &base # ========================== ## LDAP settings - # You can inspect a sample of the LDAP users with login access by running: + # You can test connections and inspect a sample of the LDAP users with login + # access by running: # bundle exec rake gitlab:ldap:check RAILS_ENV=production ldap: enabled: false From 6f5f80592499dace8900961e0c596771d46d588c Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 13 Jun 2017 15:25:06 -0700 Subject: [PATCH 121/143] Add net-ldap explicitly since we use it directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And this has already been done on EE, so it’ll match now. --- Gemfile | 1 + Gemfile.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/Gemfile b/Gemfile index 7bb912571f4..b9d206c2500 100644 --- a/Gemfile +++ b/Gemfile @@ -62,6 +62,7 @@ gem 'browser', '~> 2.2' # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master gem 'gitlab_omniauth-ldap', '~> 2.0.2', require: 'omniauth-ldap' +gem 'net-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order diff --git a/Gemfile.lock b/Gemfile.lock index 661f0294646..c21b8d43b32 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1013,6 +1013,7 @@ DEPENDENCIES minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.4.5) + net-ldap nokogiri (~> 1.6.7, >= 1.6.7.2) oauth2 (~> 1.4) octokit (~> 4.6.2) From c4854426654949feef4085fd3026d5862f00aa7c Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 26 Jul 2017 03:20:02 -0700 Subject: [PATCH 122/143] Fix project wiki web_url spec --- spec/models/project_wiki_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 1f314791479..79ab50c1234 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -21,7 +21,7 @@ describe ProjectWiki, models: true do describe '#web_url' do it 'returns the full web URL to the wiki' do - expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/wikis/home") + expect(subject.web_url).to match("https?://[^\/]+/#{project.path_with_namespace}/wikis/home") end end From f8cd9aeb26c290eb92274b63426eb3b809693e9d Mon Sep 17 00:00:00 2001 From: Max Raab Date: Wed, 26 Jul 2017 07:22:57 +0000 Subject: [PATCH 123/143] Add missing colon --- app/views/admin/dashboard/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 128b5dc01ab..8e94e68bc11 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -150,7 +150,7 @@ .well-segment.well-centered = link_to admin_groups_path do %h3.text-center - Groups + Groups: = number_with_delimiter(Group.count) %hr = link_to 'New group', new_admin_group_path, class: "btn btn-new" From 7ce0a61a993f36f30692f93c477f671f82dbef51 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Wed, 26 Jul 2017 13:23:27 +0200 Subject: [PATCH 124/143] use `.zero?` instead of `== 0` --- lib/gitlab/health_checks/fs_shards_check.rb | 8 ++++---- spec/lib/gitlab/health_checks/fs_shards_check_spec.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index ddd1aaa7043..928fb014ef4 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -81,7 +81,7 @@ module Gitlab def delete_test_file(tmp_path) _, status = exec_with_timeout(%W{ rm -f #{tmp_path} }) - status == 0 + status.zero? rescue Errno::ENOENT File.delete(tmp_path) rescue Errno::ENOENT end @@ -90,7 +90,7 @@ module Gitlab stat_path = File.join(storage_path(storage_name), '.') begin _, status = exec_with_timeout(%W{ stat #{stat_path} }) - status == 0 + status.zero? rescue Errno::ENOENT File.exist?(stat_path) && File::Stat.new(stat_path).readable? end @@ -100,7 +100,7 @@ module Gitlab _, status = exec_with_timeout(%W{ tee #{tmp_path} }) do |stdin| stdin.write(RANDOM_STRING) end - status == 0 + status.zero? rescue Errno::ENOENT written_bytes = File.write(tmp_path, RANDOM_STRING) rescue Errno::ENOENT written_bytes == RANDOM_STRING.length @@ -110,7 +110,7 @@ module Gitlab _, status = exec_with_timeout(%W{ diff #{tmp_path} - }) do |stdin| stdin.write(RANDOM_STRING) end - status == 0 + status.zero? rescue Errno::ENOENT file_contents = File.read(tmp_path) rescue Errno::ENOENT file_contents == RANDOM_STRING diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 0a8dfa3bbdd..8abc4320c59 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::HealthChecks::FsShardsCheck do def command_exists?(command) _, status = Gitlab::Popen.popen(%W{ #{command} 1 echo }) - status == 0 + status.zero? rescue Errno::ENOENT false end From b5bdc55d239f3e19f8fe1e59b118da05ac81a0dd Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Tue, 18 Jul 2017 16:09:14 +0100 Subject: [PATCH 125/143] Move exception handling to execute --- app/services/projects/destroy_service.rb | 57 ++++++++++++------- app/views/projects/_deletion_failed.html.haml | 13 ++--- app/views/projects/_flash_messages.html.haml | 5 +- app/views/projects/empty.html.haml | 1 - app/views/projects/show.html.haml | 1 - app/workers/project_destroy_worker.rb | 2 +- ...project-destroy-clean-up-after-failure.yml | 4 ++ spec/features/projects/show_project_spec.rb | 18 ++---- .../services/projects/destroy_service_spec.rb | 37 ++++++------ spec/workers/project_destroy_worker_spec.rb | 19 +++---- 10 files changed, 81 insertions(+), 76 deletions(-) create mode 100644 changelogs/unreleased/29289-project-destroy-clean-up-after-failure.yml diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 7e38aacc91a..f6e8b6655f2 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -15,29 +15,48 @@ module Projects def execute return false unless can?(current_user, :remove_project, project) - repo_path = project.path_with_namespace - wiki_path = repo_path + '.wiki' - # Flush the cache for both repositories. This has to be done _before_ # removing the physical repositories as some expiration code depends on # Git data (e.g. a list of branch names). - flush_caches(project, wiki_path) + flush_caches(project) Projects::UnlinkForkService.new(project, current_user).execute - attempt_destroy_transaction(project, repo_path, wiki_path) + attempt_destroy_transaction(project) system_hook_service.execute_hooks_for(project, :destroy) - log_info("Project \"#{project.full_path}\" was removed") + true - rescue Projects::DestroyService::DestroyError => error - Rails.logger.error("Deletion failed on #{project.full_path} with the following message: #{error.message}") + rescue => error + attempt_rollback(project, error.message) false + rescue Exception => error # rubocop:disable Lint/RescueException + # Project.transaction can raise Exception + attempt_rollback(project, error.message) + raise end private + def repo_path + project.path_with_namespace + end + + def wiki_path + repo_path + '.wiki' + end + + def trash_repositories! + unless remove_repository(repo_path) + raise_error('Failed to remove project repository. Please try again or contact administrator.') + end + + unless remove_repository(wiki_path) + raise_error('Failed to remove wiki repository. Please try again or contact administrator.') + end + end + def remove_repository(path) # Skip repository removal. We use this flag when remove user or group return true if params[:skip_repo] == true @@ -59,26 +78,24 @@ module Projects end end - def attempt_destroy_transaction(project, repo_path, wiki_path) + def attempt_rollback(project, message) + return unless project + + project.update_attributes(delete_error: message, pending_delete: false) + log_error("Deletion failed on #{project.full_path} with the following message: #{message}") + end + + def attempt_destroy_transaction(project) Project.transaction do unless remove_legacy_registry_tags raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') end - unless remove_repository(repo_path) - raise_error('Failed to remove project repository. Please try again or contact administrator.') - end - - unless remove_repository(wiki_path) - raise_error('Failed to remove wiki repository. Please try again or contact administrator.') - end + trash_repositories! project.team.truncate project.destroy! end - rescue Exception => error # rubocop:disable Lint/RescueException - project.update_attributes(delete_error: error.message, pending_delete: false) - raise end ## @@ -107,7 +124,7 @@ module Projects "#{path}+#{project.id}#{DELETED_FLAG}" end - def flush_caches(project, wiki_path) + def flush_caches(project) project.repository.before_delete Repository.new(wiki_path, project).before_delete diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml index 028510b5671..4f3698f91e6 100644 --- a/app/views/projects/_deletion_failed.html.haml +++ b/app/views/projects/_deletion_failed.html.haml @@ -1,9 +1,6 @@ -- if @project.delete_error.present? - .project-deletion-failed-message.alert.alert-warning - This project was scheduled for deletion, but failed with the following message: - = @project.delete_error +- project = local_assigns.fetch(:project) +- return unless project.delete_error.present? - .alert-link-group - = link_to "Don't show again", profile_path(user: { hide_no_ssh_key: true }), method: :put, class: 'alert-link' - | - = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' +.project-deletion-failed-message.alert.alert-warning + This project was scheduled for deletion, but failed with the following message: + = project.delete_error diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml index 6c9d466c761..f47d84ef755 100644 --- a/app/views/projects/_flash_messages.html.haml +++ b/app/views/projects/_flash_messages.html.haml @@ -1,5 +1,8 @@ +- project = local_assigns.fetch(:project) +- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message + = content_for flash_message_container do - = render 'deletion_failed' + = render partial: 'deletion_failed', locals: { project: project } - if current_user && can?(current_user, :download_code, project) = render 'shared/no_ssh' = render 'shared/no_password' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 3d7c72ae61a..d17709380d5 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,5 +1,4 @@ - @no_container = true -- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message = render partial: 'flash_messages', locals: { project: @project } diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 3926149e790..a9b39cedb1d 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,7 +1,6 @@ - @no_container = true - breadcrumb_title "Project" - @content_class = "limit-container-width" unless fluid_layout -- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index e695ec060f0..a9188b78460 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -8,6 +8,6 @@ class ProjectDestroyWorker ::Projects::DestroyService.new(project, user, params.symbolize_keys).execute rescue ActiveRecord::RecordNotFound => error - logger.error("Failed to delete project #{project.path_with_namespace} (#{project.id}): #{error.message}") + logger.error("Failed to delete project (#{project_id}): #{error.message}") end end diff --git a/changelogs/unreleased/29289-project-destroy-clean-up-after-failure.yml b/changelogs/unreleased/29289-project-destroy-clean-up-after-failure.yml new file mode 100644 index 00000000000..488b37ac37f --- /dev/null +++ b/changelogs/unreleased/29289-project-destroy-clean-up-after-failure.yml @@ -0,0 +1,4 @@ +--- +title: Handle errors while a project is being deleted asynchronously. +merge_request: 11088 +author: diff --git a/spec/features/projects/show_project_spec.rb b/spec/features/projects/show_project_spec.rb index 5aa0d8f0026..1bc6fae9e7f 100644 --- a/spec/features/projects/show_project_spec.rb +++ b/spec/features/projects/show_project_spec.rb @@ -3,28 +3,18 @@ require 'spec_helper' describe 'Project show page', feature: true do context 'when project pending delete' do let(:project) { create(:project, :empty_repo, pending_delete: true) } - let(:worker) { ProjectDestroyWorker.new } before do sign_in(project.owner) end - it 'shows flash error if deletion for project fails' do - error_message = "some error message" - project.update_attributes(delete_error: error_message, pending_delete: false) + it 'shows error message if deletion for project fails' do + project.update_attributes(delete_error: "Something went wrong", pending_delete: false) - visit namespace_project_path(project.namespace, project) + visit project_path(project) expect(page).to have_selector('.project-deletion-failed-message') - expect(page).to have_content("This project was scheduled for deletion, but failed with the following message: #{error_message}") - end - - it 'renders 404 if project was successfully deleted' do - worker.perform(project.id, project.owner.id, {}) - - visit namespace_project_path(project.namespace, project) - - expect(page).to have_http_status(404) + expect(page).to have_content("This project was scheduled for deletion, but failed with the following message: #{project.delete_error}") end end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index a629afe723d..357e09bee95 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -130,30 +130,29 @@ describe Projects::DestroyService, services: true do it_behaves_like 'handles errors thrown during async destroy', "Failed to remove project repository" end - context 'when `execute` raises any other error' do + context 'when `execute` raises expected error' do before do - expect_any_instance_of(Projects::DestroyService) - .to receive(:execute).and_raise(ArgumentError.new("Other error message")) + expect_any_instance_of(Project) + .to receive(:destroy!).and_raise(StandardError.new("Other error message")) end it_behaves_like 'handles errors thrown during async destroy', "Other error message" end - end - end - context 'with execute' do - it_behaves_like 'deleting the project with pipeline and build' + context 'when `execute` raises unexpected error' do + before do + expect_any_instance_of(Project) + .to receive(:destroy!).and_raise(Exception.new("Other error message")) + end - context 'when `execute` raises an error' do - before do - expect_any_instance_of(Projects::DestroyService) - .to receive(:execute).and_raise(ArgumentError) - end + it 'allows error to bubble up and rolls back project deletion' do + expect do + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end.to raise_error - it 'allows the error to bubble up' do - expect do - Sidekiq::Testing.inline! { Projects::DestroyService.new(project, user, {}).execute } - end.to raise_error(ArgumentError) + expect(project.reload.pending_delete).to be(false) + expect(project.delete_error).to include("Other error message") + end end end end @@ -182,8 +181,7 @@ describe Projects::DestroyService, services: true do expect_any_instance_of(ContainerRepository) .to receive(:delete_tags!).and_return(false) - expect{ destroy_project(project, user) } - .to raise_error(ActiveRecord::RecordNotDestroyed) + expect(destroy_project(project, user)).to be false end end end @@ -208,8 +206,7 @@ describe Projects::DestroyService, services: true do expect_any_instance_of(ContainerRepository) .to receive(:delete_tags!).and_return(false) - expect { destroy_project(project, user) } - .to raise_error(Projects::DestroyService::DestroyError) + expect(destroy_project(project, user)).to be false end end end diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 29f0295de42..f19c9dff941 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -21,17 +21,16 @@ describe ProjectDestroyWorker do expect(Dir.exist?(path)).to be_truthy end - describe 'when StandardError is raised' do - it 'reverts pending_delete attribute with a error message' do - allow_any_instance_of(::Projects::DestroyService).to receive(:execute).and_raise(StandardError, "some error message") + it 'does not raise error when project could not be found' do + expect do + subject.perform(-1, project.owner.id, {}) + end.not_to raise_error + end - expect do - subject.perform(project.id, project.owner.id, {}) - end.to change { project.reload.pending_delete }.from(true).to(false) - - expect(Project.all).to include(project) - expect(project.delete_error).to eq("some error message") - end + it 'does not raise error when user could not be found' do + expect do + subject.perform(project.id, -1, {}) + end.not_to raise_error end end end From 396b8f91ec47ffb5a02ebf6d713ef4cbf04f1f94 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Tue, 25 Jul 2017 17:57:02 +0100 Subject: [PATCH 126/143] Fix saving diffs that are not valid UTF-8 Previously, we used Psych, which would: 1. Check if a string was encoded as binary, and not ASCII-compatible. 2. Add the !binary tag in that case. 3. Convert to base64. We need to do the same thing, using a new column in place of the tag. --- app/models/merge_request_diff.rb | 17 +++++++++--- app/models/merge_request_diff_file.rb | 10 +++++++ ...-binary-file-with-non-utf-8-characters.yml | 5 ++++ ..._add_binary_to_merge_request_diff_files.rb | 9 +++++++ db/schema.rb | 3 ++- spec/features/projects/ref_switcher_spec.rb | 4 +-- .../import_export/safe_model_attributes.yml | 1 + spec/models/merge_request_diff_file_spec.rb | 27 ++++++++++++++++++- spec/models/merge_request_diff_spec.rb | 9 +++++++ spec/support/test_env.rb | 3 ++- 10 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/35539-can-t-create-a-merge-request-containing-a-binary-file-with-non-utf-8-characters.yml create mode 100644 db/migrate/20170725145659_add_binary_to_merge_request_diff_files.rb diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 4b141945ab4..ec87aee9310 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -236,10 +236,21 @@ class MergeRequestDiff < ActiveRecord::Base def create_merge_request_diff_files(diffs) rows = diffs.map.with_index do |diff, index| - diff.to_hash.merge( + diff_hash = diff.to_hash.merge( + binary: false, merge_request_diff_id: self.id, relative_order: index ) + + # Compatibility with old diffs created with Psych. + diff_hash.tap do |hash| + diff_text = hash[:diff] + + if diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only? + hash[:binary] = true + hash[:diff] = [diff_text].pack('m0') + end + end end Gitlab::Database.bulk_insert('merge_request_diff_files', rows) @@ -268,9 +279,7 @@ class MergeRequestDiff < ActiveRecord::Base st_diffs end elsif merge_request_diff_files.present? - merge_request_diff_files - .as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS) - .map(&:with_indifferent_access) + merge_request_diff_files.map(&:to_hash) end end diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb index 598ebd4d829..1199ff5af22 100644 --- a/app/models/merge_request_diff_file.rb +++ b/app/models/merge_request_diff_file.rb @@ -8,4 +8,14 @@ class MergeRequestDiffFile < ActiveRecord::Base encode_utf8(diff) if diff.respond_to?(:encoding) end + + def diff + binary? ? super.unpack('m0').first : super + end + + def to_hash + keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff] + + as_json(only: keys).merge(diff: diff).with_indifferent_access + end end diff --git a/changelogs/unreleased/35539-can-t-create-a-merge-request-containing-a-binary-file-with-non-utf-8-characters.yml b/changelogs/unreleased/35539-can-t-create-a-merge-request-containing-a-binary-file-with-non-utf-8-characters.yml new file mode 100644 index 00000000000..8d92aacc9ef --- /dev/null +++ b/changelogs/unreleased/35539-can-t-create-a-merge-request-containing-a-binary-file-with-non-utf-8-characters.yml @@ -0,0 +1,5 @@ +--- +title: Fix creating merge request diffs when diff contains bytes that are invalid + in UTF-8 +merge_request: +author: diff --git a/db/migrate/20170725145659_add_binary_to_merge_request_diff_files.rb b/db/migrate/20170725145659_add_binary_to_merge_request_diff_files.rb new file mode 100644 index 00000000000..1f5fa7e3d49 --- /dev/null +++ b/db/migrate/20170725145659_add_binary_to_merge_request_diff_files.rb @@ -0,0 +1,9 @@ +class AddBinaryToMergeRequestDiffFiles < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :merge_request_diff_files, :binary, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 61bcd8c7e95..1ec25c7d46f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170724214302) do +ActiveRecord::Schema.define(version: 20170725145659) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -748,6 +748,7 @@ ActiveRecord::Schema.define(version: 20170724214302) do t.text "new_path", null: false t.text "old_path", null: false t.text "diff", null: false + t.boolean "binary" end add_index "merge_request_diff_files", ["merge_request_diff_id", "relative_order"], name: "index_merge_request_diff_files_on_mr_diff_id_and_order", unique: true, using: :btree diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 31c7b492ab7..9f5544ac43e 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -19,14 +19,14 @@ feature 'Ref switcher', feature: true, js: true do input.set 'binary' wait_for_requests - expect(find('.dropdown-content ul')).to have_selector('li', count: 6) + expect(find('.dropdown-content ul')).to have_selector('li', count: 7) page.within '.dropdown-content ul' do input.native.send_keys :enter end end - expect(page).to have_title 'binary-encoding' + expect(page).to have_title 'add-pdf-text-binary' end it "user selects ref with special characters" do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 0f2db3380a7..11f4c16ff96 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -195,6 +195,7 @@ MergeRequestDiffFile: - a_mode - b_mode - too_large +- binary Ci::Pipeline: - id - project_id diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb index 7276f5b5061..239620ef4fc 100644 --- a/spec/models/merge_request_diff_file_spec.rb +++ b/spec/models/merge_request_diff_file_spec.rb @@ -1,8 +1,33 @@ require 'rails_helper' describe MergeRequestDiffFile, type: :model do + describe '#diff' do + let(:unpacked) { 'unpacked' } + let(:packed) { [unpacked].pack('m0') } + + before do + subject.diff = packed + end + + context 'when the diff is marked as binary' do + before do + subject.binary = true + end + + it 'unpacks from base 64' do + expect(subject.diff).to eq(unpacked) + end + end + + context 'when the diff is not marked as binary' do + it 'returns the raw diff' do + expect(subject.diff).to eq(packed) + end + end + end + describe '#utf8_diff' do - it 'does not raise error when a hash value is in binary' do + it 'does not raise error when the diff is binary' do subject.diff = "\x05\x00\x68\x65\x6c\x6c\x6f" expect { subject.utf8_diff }.not_to raise_error diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index edc2f4bb9f0..0e77752bccc 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -105,6 +105,15 @@ describe MergeRequestDiff, models: true do expect(mr_diff.empty?).to be_truthy end + + it 'saves binary diffs correctly' do + path = 'files/images/icn-time-tracking.pdf' + mr_diff = create(:merge_request, source_branch: 'add-pdf-text-binary', target_branch: 'master').merge_request_diff + diff_file = mr_diff.merge_request_diff_files.find_by(new_path: path) + + expect(diff_file).to be_binary + expect(diff_file.diff).to eq(mr_diff.compare.diffs(paths: [path]).to_a.first.diff) + end end describe '#commit_shas' do diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 0a194ca4c90..c32c05b03e2 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -41,7 +41,8 @@ module TestEnv 'csv' => '3dd0896', 'v1.1.0' => 'b83d6e3', 'add-ipython-files' => '93ee732', - 'add-pdf-file' => 'e774ebd' + 'add-pdf-file' => 'e774ebd', + 'add-pdf-text-binary' => '79faa7b' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily From 6ac0a142e00e5bf0945ea624c93bbfe54c91a14e Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Wed, 26 Jul 2017 17:16:59 +0200 Subject: [PATCH 127/143] Remove unnecessary begin/end --- lib/gitlab/health_checks/fs_shards_check.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index 928fb014ef4..a4740e9e9b7 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -67,12 +67,10 @@ module Gitlab end def with_temp_file(storage_name) - begin - temp_file_path = Dir::Tmpname.create(%w(fs_shards_check +deleted), storage_path(storage_name)) { |path| path } - yield temp_file_path - ensure - delete_test_file(temp_file_path) - end + temp_file_path = Dir::Tmpname.create(%w(fs_shards_check +deleted), storage_path(storage_name)) { |path| path } + yield temp_file_path + ensure + delete_test_file(temp_file_path) end def storage_path(storage_name) From 02139f5a775fafb3999976cdd213d7ef14e720d7 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 26 Jul 2017 12:40:59 -0400 Subject: [PATCH 128/143] Update the large table list in AddColumnWithDefaultToLargeTable cop - ci_builds -- 33 million rows, 55 GB - merge_request_diff_files -- 5 million rows, 9 GB (and growing rapidly) - merge_request_diffs -- 5 million rows, 190 GB --- .../cop/migration/add_column_with_default_to_large_table.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rubocop/cop/migration/add_column_with_default_to_large_table.rb b/rubocop/cop/migration/add_column_with_default_to_large_table.rb index 2372e6b60ea..87788b0d9c2 100644 --- a/rubocop/cop/migration/add_column_with_default_to_large_table.rb +++ b/rubocop/cop/migration/add_column_with_default_to_large_table.rb @@ -20,8 +20,11 @@ module RuboCop 'necessary'.freeze LARGE_TABLES = %i[ + ci_builds events issues + merge_request_diff_files + merge_request_diffs merge_requests namespaces notes From a5d2ce8e61fc4f606148ec0323154421111c5012 Mon Sep 17 00:00:00 2001 From: Casper Date: Wed, 26 Jul 2017 16:48:13 +0000 Subject: [PATCH 129/143] Use LDAP-attributes configured in gitlab.yml in lookup instead of just hard-coded attributes. --- lib/gitlab/ldap/adapter.rb | 2 +- spec/lib/gitlab/ldap/adapter_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 7b05290e5cc..8867a91c244 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -101,7 +101,7 @@ module Gitlab end def user_attributes - %W(#{config.uid} cn mail dn) + %W(#{config.uid} cn dn) + config.attributes['username'] + config.attributes['email'] end end end diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index 9454878b057..0f4b8dbf7b7 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::LDAP::Adapter, lib: true do expect(adapter).to receive(:ldap_search) do |arg| expect(arg[:filter].to_s).to eq('(uid=johndoe)') expect(arg[:base]).to eq('dc=example,dc=com') - expect(arg[:attributes]).to match(%w{uid cn mail dn}) + expect(arg[:attributes]).to match(%w{uid cn dn uid userid sAMAccountName mail email userPrincipalName}) end.and_return({}) adapter.users('uid', 'johndoe') @@ -26,7 +26,7 @@ describe Gitlab::LDAP::Adapter, lib: true do expect(adapter).to receive(:ldap_search).with( base: 'uid=johndoe,ou=users,dc=example,dc=com', scope: Net::LDAP::SearchScope_BaseObject, - attributes: %w{uid cn mail dn}, + attributes: %w{uid cn dn uid userid sAMAccountName mail email userPrincipalName}, filter: nil ).and_return({}) @@ -63,7 +63,7 @@ describe Gitlab::LDAP::Adapter, lib: true do it 'uses the right uid attribute when non-default' do stub_ldap_config(uid: 'sAMAccountName') expect(adapter).to receive(:ldap_search).with( - hash_including(attributes: %w{sAMAccountName cn mail dn}) + hash_including(attributes: %w{sAMAccountName cn dn uid userid sAMAccountName mail email userPrincipalName}) ).and_return({}) adapter.users('sAMAccountName', 'johndoe') From 03c11c3a333e46894030a724392bd714e9a0abdf Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Wed, 26 Jul 2017 18:43:43 +0000 Subject: [PATCH 130/143] remove extra space --- doc/administration/container_registry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 8cb0e5b1562..57e54815b68 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -112,7 +112,7 @@ GitLab from source respectively. >**Note:** Be careful to choose a port different than the one that Registry listens to (`5000` by default), -otherwise you will run into conflicts . +otherwise you will run into conflicts. --- From db2ea59f52555be8e10a60cc51aae832f5824d9d Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Wed, 26 Jul 2017 18:56:43 +0000 Subject: [PATCH 131/143] change Fogbugz button to FogBugz --- app/views/projects/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a2d7a21d5f6..87cc23fc649 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -72,7 +72,7 @@ %div - if fogbugz_import_enabled? = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - = icon('bug', text: 'Fogbugz') + = icon('bug', text: 'FogBugz') %div - if gitea_import_enabled? = link_to new_import_gitea_url, class: 'btn import_gitea' do From 830257c920362e8c2c42a8c5014f6b7083b72542 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 26 Jul 2017 16:43:41 -0500 Subject: [PATCH 132/143] Fix participant name link trailing space on milestone page --- app/views/shared/milestones/_participants_tab.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml index 549d2e2f61e..1615871385e 100644 --- a/app/views/shared/milestones/_participants_tab.html.haml +++ b/app/views/shared/milestones/_participants_tab.html.haml @@ -4,5 +4,5 @@ = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" %strong= truncate(user.name, length: 40) - %br - %small.cgray= user.username + %div + %small.cgray= user.username From 69129f11df7aa738bfed7a84eead6fdf5d25a814 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Mon, 17 Jul 2017 20:41:50 +0900 Subject: [PATCH 133/143] Remove help message about prioritized labels for non-members --- app/views/projects/labels/index.html.haml | 13 ++++++++----- .../35191-prioritized-labels-for-non-member.yml | 4 ++++ .../projects/labels/update_prioritization_spec.rb | 8 ++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/35191-prioritized-labels-for-non-member.yml diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index d02ea5cccc3..4b9da02c6b8 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,6 +1,7 @@ - @no_container = true - page_title "Labels" - hide_class = '' +- can_admin_label = can?(current_user, :admin_label, @project) - if show_new_nav? && can?(current_user, :admin_label, @project) - content_for :breadcrumbs_extra do @@ -12,15 +13,17 @@ %div{ class: container_class } .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. + Labels can be applied to issues and merge requests. + - if can_admin_label + Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. - .nav-controls{ class: ("visible-xs" if show_new_nav?) } - - if can?(current_user, :admin_label, @project) + - if can_admin_label + .nav-controls{ class: ("visible-xs" if show_new_nav?) } = link_to new_project_label_path(@project), class: "btn btn-new" do New label .labels - - if can?(current_user, :admin_label, @project) + - if can_admin_label -# Only show it in the first page - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels{ class: ('hide' if hide) } @@ -33,7 +36,7 @@ - if @labels.present? .other-labels - - if can?(current_user, :admin_label, @project) + - if can_admin_label %h5{ class: ('hide' if hide) } Other Labels %ul.content-list.manage-labels-list.js-other-labels = render partial: 'shared/label', subject: @project, collection: @labels, as: :label diff --git a/changelogs/unreleased/35191-prioritized-labels-for-non-member.yml b/changelogs/unreleased/35191-prioritized-labels-for-non-member.yml new file mode 100644 index 00000000000..fbe55d4c2b0 --- /dev/null +++ b/changelogs/unreleased/35191-prioritized-labels-for-non-member.yml @@ -0,0 +1,4 @@ +--- +title: Remove help message about prioritized labels for non-members +merge_request: 12912 +author: Takuya Noguchi diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 61f6d734ed3..9b51b427845 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -114,6 +114,12 @@ feature 'Prioritize labels', feature: true do expect(page.all('li').last).to have_content('bug') end end + + it 'shows a help message about prioritized labels' do + visit project_labels_path(project) + + expect(page).to have_content 'Star a label' + end end context 'as a guest' do @@ -128,6 +134,7 @@ feature 'Prioritize labels', feature: true do expect(page).to have_content 'wontfix' expect(page).to have_content 'feature' expect(page).not_to have_css('.prioritized-labels') + expect(page).not_to have_content 'Star a label' end end @@ -139,6 +146,7 @@ feature 'Prioritize labels', feature: true do expect(page).to have_content 'wontfix' expect(page).to have_content 'feature' expect(page).not_to have_css('.prioritized-labels') + expect(page).not_to have_content 'Star a label' end end end From 3256f6f4045661edf6816dbd0914fbedbffd2649 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 27 Jul 2017 08:41:47 +0100 Subject: [PATCH 134/143] Compile JS lang files before webpack compile Closes #35615 --- lib/tasks/gitlab/assets.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 003d57adbbd..259a755d724 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -4,6 +4,7 @@ namespace :gitlab do task compile: [ 'yarn:check', 'rake:assets:precompile', + 'gettext:po_to_json', 'webpack:compile', 'fix_urls' ] From 05e152fa7f39c3ae9492159a6c29532a27b4b40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 27 Jul 2017 09:58:06 +0200 Subject: [PATCH 135/143] Fix the :project factory by not copying the test repo twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, fixing some calls to the :project factory with the :test_repo trait since this trait is already included in the :project factory. Signed-off-by: Rémy Coutable --- spec/factories/projects.rb | 4 ---- spec/features/projects/blobs/edit_spec.rb | 2 +- spec/lib/gitlab/import_export/fork_spec.rb | 2 +- spec/lib/gitlab/import_export/merge_request_parser_spec.rb | 2 +- spec/lib/gitlab/import_export/repo_restorer_spec.rb | 2 +- spec/requests/api/todos_spec.rb | 2 +- 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1bb2db11e7f..485ed48d2de 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -171,10 +171,6 @@ FactoryGirl.define do end after :create do |project, evaluator| - TestEnv.copy_repo(project, - bare_repo: TestEnv.factory_repo_path_bare, - refs: TestEnv::BRANCH_SHA) - if evaluator.create_template args = evaluator.create_template diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index c9384a09ccd..ddd27083147 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' feature 'Editing file blob', feature: true, js: true do include TreeHelper - let(:project) { create(:project, :public, :test_repo) } + let(:project) { create(:project, :public) } let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') } let(:branch) { 'master' } let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] } diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index 70796781532..e8eb7e4f8f4 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'forked project import', services: true do let(:user) { create(:user) } - let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } + let!(:project_with_repo) { create(:project, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project) { create(:empty_project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 349be4596b6..f2b66c4421c 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::ImportExport::MergeRequestParser do let(:user) { create(:user) } - let!(:project) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } + let!(:project) { create(:project, name: 'test-repo-restorer', path: 'test-repo-restorer') } let(:forked_from_project) { create(:project) } let(:fork_link) { create(:forked_project_link, forked_from_project: project) } diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index 30b6a0d8845..09bfaa8fb75 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ImportExport::RepoRestorer, services: true do describe 'bundle a project Git repo' do let(:user) { create(:user) } - let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') } + let!(:project_with_repo) { create(:project, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project) { create(:empty_project) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 92533f4dfea..9fc73c6e092 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe API::Todos do - let(:project_1) { create(:empty_project, :test_repo) } + let(:project_1) { create(:project) } let(:project_2) { create(:empty_project) } let(:author_1) { create(:user) } let(:author_2) { create(:user) } From c58023858182b300fbbb1a6ad192f8ea975bfe98 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Thu, 27 Jul 2017 08:30:14 +0000 Subject: [PATCH 136/143] Inline JS Removal for Merge Requests --- app/assets/javascripts/dispatcher.js | 19 +++++++++++++++++++ app/assets/javascripts/how_to_merge.js | 4 ++-- .../creations/_new_compare.html.haml | 9 +-------- .../creations/_new_submit.html.haml | 7 +------ .../projects/merge_requests/show.html.haml | 15 ++++----------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ffe97c071ba..1dc6edacfed 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -20,6 +20,8 @@ /* global NamespaceSelects */ /* global Project */ /* global ProjectAvatar */ +/* global MergeRequest */ +/* global Compare */ /* global CompareAutocomplete */ /* global ProjectNew */ /* global ProjectShow */ @@ -221,6 +223,19 @@ import PerformanceBar from './performance_bar'; new gl.IssuableTemplateSelectors(); break; case 'projects:merge_requests:creations:new': + const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); + if (mrNewCompareNode) { + new Compare({ + targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, + sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, + targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, + }); + } else { + const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); + new MergeRequest({ + action: mrNewSubmitNode.dataset.mrSubmitAction, + }); + } case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:edit': new gl.Diff(); @@ -257,6 +272,10 @@ import PerformanceBar from './performance_bar'; new gl.Diff(); shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); + const mrShowNode = document.querySelector('.merge-request'); + window.mergeRequest = new MergeRequest({ + action: mrShowNode.dataset.mrAction, + }); break; case 'dashboard:activity': new gl.Activities(); diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js index f739db751a6..19f4a946f73 100644 --- a/app/assets/javascripts/how_to_merge.js +++ b/app/assets/javascripts/how_to_merge.js @@ -3,10 +3,10 @@ document.addEventListener('DOMContentLoaded', () => { modal: true, show: false, }); - $('.how_to_merge_link').bind('click', () => { + $('.how_to_merge_link').on('click', () => { modal.show(); }); - $('.modal-header .close').bind('click', () => { + $('.modal-header .close').on('click', () => { modal.hide(); }); }); diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 4e5aae496b1..8958b2cf5e1 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -3,7 +3,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors - .merge-request-branches.row + .merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) } .col-md-6 .panel.panel-default.panel-new-merge-request .panel-heading @@ -66,10 +66,3 @@ - if @merge_request.errors.any? = form_errors(@merge_request) = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" - -:javascript - new Compare({ - targetProjectUrl: "#{project_new_merge_request_update_branches_path(@source_project)}", - sourceBranchUrl: "#{project_new_merge_request_branch_from_path(@source_project)}", - targetBranchUrl: "#{project_new_merge_request_branch_to_path(@source_project)}" - }); diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index c72dd1d8e29..4b5fa28078a 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -17,7 +17,7 @@ = f.hidden_field :target_project_id = f.hidden_field :target_branch -.mr-compare.merge-request +.mr-compare.merge-request.js-merge-request-new-submit{ 'data-mr-submit-action': "#{j params[:tab].presence || 'new'}" } - if @commits.empty? .commits-empty %h4 @@ -50,8 +50,3 @@ .mr-loading-status = spinner - -:javascript - var merge_request = new MergeRequest({ - action: "#{j params[:tab].presence || 'new'}", - }); diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 2efc1d68190..ea6cd16c7ad 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -3,10 +3,10 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('diff_notes') + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('diff_notes') -.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } +.merge-request{ 'data-mr-action': "#{j params[:tab].presence || 'show'}", 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } = render "projects/merge_requests/mr_title" .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } @@ -15,13 +15,13 @@ - if @merge_request.source_branch_exists? = render "projects/merge_requests/how_to_merge" + -# haml-lint:disable InlineJavaScript :javascript window.gl.mrWidgetData = #{serialize_issuable(@merge_request)} #js-vue-mr-widget.mr-widget - content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'vue_merge_request_widget' .content-block.content-block-small.emoji-list-container @@ -88,10 +88,3 @@ = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title - if @merge_request.can_be_cherry_picked? = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title - -:javascript - $(function () { - window.mergeRequest = new MergeRequest({ - action: "#{j params[:tab].presence || 'show'}", - }); - }); From bfe8b96874c66c54e2e4c1a66a520087b217e9e7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 26 Jul 2017 12:34:52 +0200 Subject: [PATCH 137/143] Add specs --- spec/features/oauth_login_spec.rb | 2 +- .../gitlab/request_forgery_protection_spec.rb | 89 +++++++++++++++++++ spec/requests/api/helpers_spec.rb | 50 +++++++++-- spec/support/forgery_protection.rb | 11 +++ 4 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 spec/lib/gitlab/request_forgery_protection_spec.rb create mode 100644 spec/support/forgery_protection.rb diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index 0064c9ef25e..49d8e52f861 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'OAuth Login', js: true do +feature 'OAuth Login', :js, :allow_forgery_protection do include DeviseHelpers def enter_code(code) diff --git a/spec/lib/gitlab/request_forgery_protection_spec.rb b/spec/lib/gitlab/request_forgery_protection_spec.rb new file mode 100644 index 00000000000..305de613866 --- /dev/null +++ b/spec/lib/gitlab/request_forgery_protection_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe Gitlab::RequestForgeryProtection, :allow_forgery_protection do + let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) } + let(:env) do + { + 'rack.input' => '', + 'rack.session' => { + _csrf_token: csrf_token + } + } + end + + describe '.call' do + context 'when the request method is GET' do + before do + env['REQUEST_METHOD'] = 'GET' + end + + it 'does not raise an exception' do + expect { described_class.call(env) }.not_to raise_exception + end + end + + context 'when the request method is POST' do + before do + env['REQUEST_METHOD'] = 'POST' + end + + context 'when the CSRF token is valid' do + before do + env['HTTP_X_CSRF_TOKEN'] = csrf_token + end + + it 'does not raise an exception' do + expect { described_class.call(env) }.not_to raise_exception + end + end + + context 'when the CSRF token is invalid' do + before do + env['HTTP_X_CSRF_TOKEN'] = 'foo' + end + + it 'raises an ActionController::InvalidAuthenticityToken exception' do + expect { described_class.call(env) }.to raise_exception(ActionController::InvalidAuthenticityToken) + end + end + end + end + + describe '.verified?' do + context 'when the request method is GET' do + before do + env['REQUEST_METHOD'] = 'GET' + end + + it 'returns true' do + expect(described_class.verified?(env)).to be_truthy + end + end + + context 'when the request method is POST' do + before do + env['REQUEST_METHOD'] = 'POST' + end + + context 'when the CSRF token is valid' do + before do + env['HTTP_X_CSRF_TOKEN'] = csrf_token + end + + it 'returns true' do + expect(described_class.verified?(env)).to be_truthy + end + end + + context 'when the CSRF token is invalid' do + before do + env['HTTP_X_CSRF_TOKEN'] = 'foo' + end + + it 'returns false' do + expect(described_class.verified?(env)).to be_falsey + end + end + end + end +end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 25ec44fa036..7a1bd76af7a 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -10,8 +10,16 @@ describe API::Helpers do let(:key) { create(:key, user: user) } let(:params) { {} } - let(:env) { { 'REQUEST_METHOD' => 'GET' } } - let(:request) { Rack::Request.new(env) } + let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) } + let(:env) do + { + 'rack.input' => '', + 'rack.session' => { + _csrf_token: csrf_token + }, + 'REQUEST_METHOD' => 'GET' + } + end let(:header) { } before do @@ -58,7 +66,7 @@ describe API::Helpers do describe ".current_user" do subject { current_user } - describe "Warden authentication" do + describe "Warden authentication", :allow_forgery_protection do before do doorkeeper_guard_returns false end @@ -99,7 +107,17 @@ describe API::Helpers do env['REQUEST_METHOD'] = 'PUT' end - it { is_expected.to be_nil } + context 'without CSRF token' do + it { is_expected.to be_nil } + end + + context 'with CSRF token' do + before do + env['HTTP_X_CSRF_TOKEN'] = csrf_token + end + + it { is_expected.to eq(user) } + end end context "POST request" do @@ -107,7 +125,17 @@ describe API::Helpers do env['REQUEST_METHOD'] = 'POST' end - it { is_expected.to be_nil } + context 'without CSRF token' do + it { is_expected.to be_nil } + end + + context 'with CSRF token' do + before do + env['HTTP_X_CSRF_TOKEN'] = csrf_token + end + + it { is_expected.to eq(user) } + end end context "DELETE request" do @@ -115,7 +143,17 @@ describe API::Helpers do env['REQUEST_METHOD'] = 'DELETE' end - it { is_expected.to be_nil } + context 'without CSRF token' do + it { is_expected.to be_nil } + end + + context 'with CSRF token' do + before do + env['HTTP_X_CSRF_TOKEN'] = csrf_token + end + + it { is_expected.to eq(user) } + end end end end diff --git a/spec/support/forgery_protection.rb b/spec/support/forgery_protection.rb new file mode 100644 index 00000000000..a5e7b761651 --- /dev/null +++ b/spec/support/forgery_protection.rb @@ -0,0 +1,11 @@ +RSpec.configure do |config| + config.around(:each, :allow_forgery_protection) do |example| + begin + ActionController::Base.allow_forgery_protection = true + + example.call + ensure + ActionController::Base.allow_forgery_protection = false + end + end +end From cbfdc7e3b53c5443a82c78bc69c77ad000648973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 27 Jul 2017 10:46:59 +0200 Subject: [PATCH 138/143] Ensure the overriding of Gitlab::Application.routes.default_url_options is only local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/features/dashboard/issues_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 2a5ef08da60..ea7a9efc326 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -80,6 +80,8 @@ RSpec.describe 'Dashboard Issues', feature: true do end it 'shows the new issue page', js: true do + original_defaults = Gitlab::Application.routes.default_url_options + Gitlab::Application.routes.default_url_options = { host: Capybara.current_session.server.host, port: Capybara.current_session.server.port, @@ -95,6 +97,8 @@ RSpec.describe 'Dashboard Issues', feature: true do page.within('#content-body') do expect(page).to have_selector('.issue-form') end + + Gitlab::Application.routes.default_url_options = original_defaults end end end From d27dec80ceb372a5aace7c69e3bdba841d1ed863 Mon Sep 17 00:00:00 2001 From: Markus Koller Date: Thu, 20 Jul 2017 10:50:07 +0200 Subject: [PATCH 139/143] Support custom directory in gitlab:backup:create task --- .../unreleased/feature-backup-custom-path.yml | 4 ++ config/initializers/1_settings.rb | 4 -- doc/raketasks/backup_restore.md | 9 ++++ lib/backup/manager.rb | 42 ++++++++++----- spec/lib/gitlab/backup/manager_spec.rb | 52 +++++++++++++++++++ spec/support/stub_configuration.rb | 29 ++++++++--- 6 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 changelogs/unreleased/feature-backup-custom-path.yml diff --git a/changelogs/unreleased/feature-backup-custom-path.yml b/changelogs/unreleased/feature-backup-custom-path.yml new file mode 100644 index 00000000000..1c5f25b3ee5 --- /dev/null +++ b/changelogs/unreleased/feature-backup-custom-path.yml @@ -0,0 +1,4 @@ +--- +title: Support custom directory in gitlab:backup:create task +merge_request: 12984 +author: Markus Koller diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 201a1d062b9..02d3161f769 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -459,10 +459,6 @@ Settings.backup['pg_schema'] = nil Settings.backup['path'] = Settings.absolute(Settings.backup['path'] || "tmp/backups/") Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) -# Convert upload connection settings to use symbol keys, to make Fog happy -if Settings.backup['upload']['connection'] - Settings.backup['upload']['connection'] = Hash[Settings.backup['upload']['connection'].map { |k, v| [k.to_sym, v] }] -end Settings.backup['upload']['multipart_chunk_size'] ||= 104857600 Settings.backup['upload']['encryption'] ||= nil Settings.backup['upload']['storage_class'] ||= nil diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 855f437cd73..6ccd79641bc 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -270,6 +270,15 @@ For installations from source: remote_directory: 'gitlab_backups' ``` +### Specifying a custom directory for backups + +If you want to group your backups you can pass a `DIRECTORY` environment variable: + +``` +sudo gitlab-rake gitlab:backup:create DIRECTORY=daily +sudo gitlab-rake gitlab:backup:create DIRECTORY=weekly +``` + ### Backup archive permissions The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`) diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index f755c99ea4a..ca6d6848d41 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -8,18 +8,9 @@ module Backup # Make sure there is a connection ActiveRecord::Base.connection.reconnect! - # saving additional informations - s = {} - s[:db_version] = "#{ActiveRecord::Migrator.current_version}" - s[:backup_created_at] = Time.now - s[:gitlab_version] = Gitlab::VERSION - s[:tar_version] = tar_version - s[:skipped] = ENV["SKIP"] - tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{s[:gitlab_version]}#{FILE_NAME_SUFFIX}" - Dir.chdir(backup_path) do File.open("#{backup_path}/backup_information.yml", "w+") do |file| - file << s.to_yaml.gsub(/^---\n/, '') + file << backup_information.to_yaml.gsub(/^---\n/, '') end # create archive @@ -33,11 +24,11 @@ module Backup abort 'Backup failed' end - upload(tar_file) + upload end end - def upload(tar_file) + def upload $progress.print "Uploading backup archive to remote storage #{remote_directory} ... " connection_settings = Gitlab.config.backup.upload.connection @@ -48,7 +39,7 @@ module Backup directory = connect_to_remote_directory(connection_settings) - if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, + if directory.files.create(key: remote_target, body: File.open(tar_file), public: false, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, encryption: Gitlab.config.backup.upload.encryption, storage_class: Gitlab.config.backup.upload.storage_class) @@ -177,7 +168,8 @@ module Backup end def connect_to_remote_directory(connection_settings) - connection = ::Fog::Storage.new(connection_settings) + # our settings use string keys, but Fog expects symbols + connection = ::Fog::Storage.new(connection_settings.symbolize_keys) # We only attempt to create the directory for local backups. For AWS # and other cloud providers, we cannot guarantee the user will have @@ -193,6 +185,14 @@ module Backup Gitlab.config.backup.upload.remote_directory end + def remote_target + if ENV['DIRECTORY'] + File.join(ENV['DIRECTORY'], tar_file) + else + tar_file + end + end + def backup_contents folders_to_backup + archives_to_backup + ["backup_information.yml"] end @@ -214,5 +214,19 @@ module Backup def settings @settings ||= YAML.load_file("backup_information.yml") end + + def tar_file + @tar_file ||= "#{backup_information[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{backup_information[:gitlab_version]}#{FILE_NAME_SUFFIX}" + end + + def backup_information + @backup_information ||= { + db_version: ActiveRecord::Migrator.current_version.to_s, + backup_created_at: Time.now, + gitlab_version: Gitlab::VERSION, + tar_version: tar_version, + skipped: ENV["SKIP"] + } + end end end diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb index 1c3d2547fec..8536d152272 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -214,4 +214,56 @@ describe Backup::Manager, lib: true do end end end + + describe '#upload' do + let(:backup_file) { Tempfile.new('backup', Gitlab.config.backup.path) } + let(:backup_filename) { File.basename(backup_file.path) } + + before do + allow(subject).to receive(:tar_file).and_return(backup_filename) + + stub_backup_setting( + upload: { + connection: { + provider: 'AWS', + aws_access_key_id: 'id', + aws_secret_access_key: 'secret' + }, + remote_directory: 'directory', + multipart_chunk_size: 104857600, + encryption: nil, + storage_class: nil + } + ) + + # the Fog mock only knows about directories we create explicitly + Fog.mock! + connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) + connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) + end + + context 'target path' do + it 'uses the tar filename by default' do + expect_any_instance_of(Fog::Collection).to receive(:create) + .with(hash_including(key: backup_filename)) + .and_return(true) + + Dir.chdir(Gitlab.config.backup.path) do + subject.upload + end + end + + it 'adds the DIRECTORY environment variable if present' do + stub_env('DIRECTORY', 'daily') + + expect_any_instance_of(Fog::Collection).to receive(:create) + .with(hash_including(key: "daily/#{backup_filename}")) + .and_return(true) + + Dir.chdir(Gitlab.config.backup.path) do + subject.upload + end + end + end + end end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 80ecce92dc1..516f8878679 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -4,9 +4,9 @@ module StubConfiguration # Stubbing both of these because we're not yet consistent with how we access # current application settings - allow_any_instance_of(ApplicationSetting).to receive_messages(messages) + allow_any_instance_of(ApplicationSetting).to receive_messages(to_settings(messages)) allow(Gitlab::CurrentSettings.current_application_settings) - .to receive_messages(messages) + .to receive_messages(to_settings(messages)) end def stub_not_protect_default_branch @@ -15,23 +15,27 @@ module StubConfiguration end def stub_config_setting(messages) - allow(Gitlab.config.gitlab).to receive_messages(messages) + allow(Gitlab.config.gitlab).to receive_messages(to_settings(messages)) end def stub_gravatar_setting(messages) - allow(Gitlab.config.gravatar).to receive_messages(messages) + allow(Gitlab.config.gravatar).to receive_messages(to_settings(messages)) end def stub_incoming_email_setting(messages) - allow(Gitlab.config.incoming_email).to receive_messages(messages) + allow(Gitlab.config.incoming_email).to receive_messages(to_settings(messages)) end def stub_mattermost_setting(messages) - allow(Gitlab.config.mattermost).to receive_messages(messages) + allow(Gitlab.config.mattermost).to receive_messages(to_settings(messages)) end def stub_omniauth_setting(messages) - allow(Gitlab.config.omniauth).to receive_messages(messages) + allow(Gitlab.config.omniauth).to receive_messages(to_settings(messages)) + end + + def stub_backup_setting(messages) + allow(Gitlab.config.backup).to receive_messages(to_settings(messages)) end private @@ -54,4 +58,15 @@ module StubConfiguration messages[predicate.to_sym] = messages[key.to_sym] end end + + # Support nested hashes by converting all values into Settingslogic objects + def to_settings(hash) + hash.transform_values do |value| + if value.is_a? Hash + Settingslogic.new(value.deep_stringify_keys) + else + value + end + end + end end From fa3e4ddff0adf8dd678cba7d5671117b8f66db75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 27 Jul 2017 12:27:09 +0000 Subject: [PATCH 140/143] Remove mentions of SeanPackham since he's no longer with GitLab --- doc/development/doc_styleguide.md | 4 ++-- doc/university/process/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 36c55cbaceb..90d1d9657b9 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -105,8 +105,8 @@ merge request. considered beta or experimental, put this info in a note, not in the heading. - When introducing a new document, be careful for the headings to be grammatically and syntactically correct. It is advised to mention one or all - of the following GitLab members for a review: `@axil`, `@rspeicher`, `@marcia`, - `@SeanPackham`. This is to ensure that no document with wrong heading is going + of the following GitLab members for a review: `@axil`, `@rspeicher`, `@marcia`. + This is to ensure that no document with wrong heading is going live without an audit, thus preventing dead links and redirection issues when corrected - Leave exactly one newline after a heading diff --git a/doc/university/process/README.md b/doc/university/process/README.md index 7ff53c2cc3f..04f2d52514f 100644 --- a/doc/university/process/README.md +++ b/doc/university/process/README.md @@ -27,4 +27,4 @@ please submit a merge request to add an upcoming class, assign to 1. Please upload any video recordings to our Youtube channel. We prefer them to be public, if needed they can be unlisted but if so they should be linked from this page. -1. Please create a merge request and assign to [SeanPackham](https://gitlab.com/u/SeanPackham). +1. Please create a merge request and assign to [Erica](https://gitlab.com/u/Erica). From 649382b1c27b09e5b54a5cdb21f078f37ecd7306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 26 Jul 2017 14:33:09 +0200 Subject: [PATCH 141/143] Fix the /projects/:id/repository/branches endpoint to handle dots in the branch name when the project full patch contains a `/` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../rc-fix-branches-api-endpoint.yml | 5 + doc/api/README.md | 13 +- lib/api/branches.rb | 14 +- .../api/schemas/public_api/v4/branch.json | 20 + .../api/schemas/public_api/v4/branches.json | 4 + .../schemas/public_api/v4/commit/basic.json | 37 ++ spec/requests/api/branches_spec.rb | 578 ++++++++++-------- spec/requests/api/groups_spec.rb | 2 +- spec/requests/api/projects_spec.rb | 2 +- spec/requests/api/v3/groups_spec.rb | 2 +- spec/requests/api/v3/projects_spec.rb | 2 +- spec/support/api/schema_matcher.rb | 9 +- .../requests}/api/status_shared_examples.rb | 6 +- 13 files changed, 436 insertions(+), 258 deletions(-) create mode 100644 changelogs/unreleased/rc-fix-branches-api-endpoint.yml create mode 100644 spec/fixtures/api/schemas/public_api/v4/branch.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/branches.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/commit/basic.json rename spec/support/{ => shared_examples/requests}/api/status_shared_examples.rb (80%) diff --git a/changelogs/unreleased/rc-fix-branches-api-endpoint.yml b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml new file mode 100644 index 00000000000..217dfff8c63 --- /dev/null +++ b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Fix the /projects/:id/repository/branches endpoint to handle dots in the branch + name when the project full patch contains a `/` +merge_request: +author: diff --git a/doc/api/README.md b/doc/api/README.md index a888c0ebb4e..fe29563eaca 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -340,7 +340,18 @@ URL-encoded. For example, `/` is represented by `%2F`: ``` -/api/v4/projects/diaspora%2Fdiaspora +GET /api/v4/projects/diaspora%2Fdiaspora +``` + +## Branches & tags name encoding + +If your branch or tag contains a `/`, make sure the branch/tag name is +URL-encoded. + +For example, `/` is represented by `%2F`: + +``` +GET /api/v4/projects/1/branches/my%2Fbranch/commits ``` ## `id` vs `iid` diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 3d816f8771d..cc81f3898ab 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -16,7 +16,7 @@ module API params do use :pagination end - get ":id/repository/branches" do + get ':id/repository/branches' do branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) present paginate(branches), with: Entities::RepoBranch, project: user_project @@ -28,7 +28,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do + get ':id/repository/branches/:branch', requirements: { id: %r{[^/]+}, branch: %r{[^/]+} } do branch = user_project.repository.find_branch(params[:branch]) not_found!("Branch") unless branch @@ -46,7 +46,7 @@ module API optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch' optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch' end - put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do + put ':id/repository/branches/:branch/protect', requirements: { id: %r{[^/]+}, branch: %r{[^/]+} } do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -81,7 +81,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do + put ':id/repository/branches/:branch/unprotect', requirements: { id: %r{[^/]+}, branch: %r{[^/]+} } do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -99,7 +99,7 @@ module API requires :branch, type: String, desc: 'The name of the branch' requires :ref, type: String, desc: 'Create branch from commit sha or existing branch' end - post ":id/repository/branches" do + post ':id/repository/branches' do authorize_push_project result = CreateBranchService.new(user_project, current_user) @@ -118,7 +118,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do + delete ':id/repository/branches/:branch', requirements: { id: %r{[^/]+}, branch: %r{[^/]+} } do authorize_push_project result = DeleteBranchService.new(user_project, current_user) @@ -130,7 +130,7 @@ module API end desc 'Delete all merged branches' - delete ":id/repository/merged_branches" do + delete ':id/repository/merged_branches' do DeleteMergedBranchesService.new(user_project, current_user).async_execute accepted! diff --git a/spec/fixtures/api/schemas/public_api/v4/branch.json b/spec/fixtures/api/schemas/public_api/v4/branch.json new file mode 100644 index 00000000000..a3581178974 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/branch.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "required" : [ + "name", + "commit", + "merged", + "protected", + "developers_can_push", + "developers_can_merge" + ], + "properties" : { + "name": { "type": "string" }, + "commit": { "$ref": "commit/basic.json" }, + "merged": { "type": "boolean" }, + "protected": { "type": "boolean" }, + "developers_can_push": { "type": "boolean" }, + "developers_can_merge": { "type": "boolean" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/branches.json b/spec/fixtures/api/schemas/public_api/v4/branches.json new file mode 100644 index 00000000000..854c902b485 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/branches.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "branch.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/commit/basic.json b/spec/fixtures/api/schemas/public_api/v4/commit/basic.json new file mode 100644 index 00000000000..9d99628a286 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/commit/basic.json @@ -0,0 +1,37 @@ +{ + "type": "object", + "required" : [ + "id", + "short_id", + "title", + "created_at", + "parent_ids", + "message", + "author_name", + "author_email", + "authored_date", + "committer_name", + "committer_email", + "committed_date" + ], + "properties" : { + "id": { "type": ["string", "null"] }, + "short_id": { "type": ["string", "null"] }, + "title": { "type": "string" }, + "created_at": { "type": "date" }, + "parent_ids": { + "type": ["array", "null"], + "items": { + "type": "string", + "additionalProperties": false + } + }, + "message": { "type": "string" }, + "author_name": { "type": "string" }, + "author_email": { "type": "string" }, + "authored_date": { "type": "date" }, + "committer_name": { "type": "string" }, + "committer_email": { "type": "string" }, + "committed_date": { "type": "date" } + } +} diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index c64499fc8c0..5a2e1b2cf2d 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -1,25 +1,31 @@ require 'spec_helper' -require 'mime/types' describe API::Branches do let(:user) { create(:user) } - let!(:project) { create(:project, :repository, creator: user) } - let!(:master) { create(:project_member, :master, user: user, project: project) } - let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } - let!(:branch_name) { 'feature' } - let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] } + let(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let(:project) { create(:project, :repository, creator: user, path: 'my.project') } + let(:branch_name) { 'feature' } + let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + let(:branch_with_dot) { project.repository.find_branch('ends-with.json') } + let(:branch_with_slash) { project.repository.find_branch('improve/awesome') } + + let(:project_id) { project.id } + let(:current_user) { nil } + + before do + project.add_master(user) + end describe "GET /projects/:id/repository/branches" do - let(:route) { "/projects/#{project.id}/repository/branches" } + let(:route) { "/projects/#{project_id}/repository/branches" } shared_examples_for 'repository branches' do it 'returns the repository branches' do get api(route, current_user), per_page: 100 - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branches') expect(response).to include_pagination_headers - expect(json_response).to be_an Array branch_names = json_response.map { |x| x['name'] } expect(branch_names).to match_array(project.repository.branch_names) end @@ -34,10 +40,9 @@ describe API::Branches do end context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository branches' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end + let(:project) { create(:project, :public, :repository) } + + it_behaves_like 'repository branches' end context 'when unauthenticated', 'and project is private' do @@ -47,9 +52,15 @@ describe API::Branches do end end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository branches' do - let(:current_user) { user } + context 'when authenticated', 'as a master' do + let(:current_user) { user } + + it_behaves_like 'repository branches' + + context 'requesting with the escaped project full path' do + let(:project_id) { CGI.escape(project.full_path) } + + it_behaves_like 'repository branches' end end @@ -61,31 +72,15 @@ describe API::Branches do end describe "GET /projects/:id/repository/branches/:branch" do - let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" } + let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}" } - shared_examples_for 'repository branch' do |merged: false| + shared_examples_for 'repository branch' do it 'returns the repository branch' do get api(route, current_user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['merged']).to eq(merged) - expect(json_response['protected']).to eq(false) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - - json_commit = json_response['commit'] - expect(json_commit['id']).to eq(branch_sha) - expect(json_commit).to have_key('short_id') - expect(json_commit).to have_key('title') - expect(json_commit).to have_key('message') - expect(json_commit).to have_key('author_name') - expect(json_commit).to have_key('author_email') - expect(json_commit).to have_key('authored_date') - expect(json_commit).to have_key('committer_name') - expect(json_commit).to have_key('committer_email') - expect(json_commit).to have_key('committed_date') - expect(json_commit).to have_key('parent_ids') + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(CGI.unescape(branch_name)) end context 'when branch does not exist' do @@ -107,10 +102,9 @@ describe API::Branches do end context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository branch' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end + let(:project) { create(:project, :public, :repository) } + + it_behaves_like 'repository branch' end context 'when unauthenticated', 'and project is private' do @@ -120,22 +114,41 @@ describe API::Branches do end end - context 'when authenticated', 'as a developer' do + context 'when authenticated', 'as a master' do let(:current_user) { user } + it_behaves_like 'repository branch' context 'when branch contains a dot' do let(:branch_name) { branch_with_dot.name } - let(:branch_sha) { project.commit('master').sha } it_behaves_like 'repository branch' end - context 'when branch is merged' do - let(:branch_name) { 'merge-test' } - let(:branch_sha) { project.commit('merge-test').sha } + context 'when branch contains a slash' do + let(:branch_name) { branch_with_slash.name } - it_behaves_like 'repository branch', merged: true + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + end + end + + context 'when branch contains an escaped slash' do + let(:branch_name) { CGI.escape(branch_with_slash.name) } + + it_behaves_like 'repository branch' + end + + context 'requesting with the escaped project full path' do + let(:project_id) { CGI.escape(project.full_path) } + + it_behaves_like 'repository branch' + + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot.name } + + it_behaves_like 'repository branch' + end end end @@ -147,268 +160,348 @@ describe API::Branches do end describe 'PUT /projects/:id/repository/branches/:branch/protect' do - context "when a protected branch doesn't already exist" do + let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}/protect" } + + shared_examples_for 'repository new protected branch' do it 'protects a single branch' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) + put api(route, current_user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - end - - it "protects a single branch with dots in the name" do - put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user) - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_with_dot.name) + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(CGI.unescape(branch_name)) expect(json_response['protected']).to eq(true) end it 'protects a single branch and developers can push' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: true + put api(route, current_user), developers_can_push: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(CGI.unescape(branch_name)) expect(json_response['protected']).to eq(true) expect(json_response['developers_can_push']).to eq(true) expect(json_response['developers_can_merge']).to eq(false) end it 'protects a single branch and developers can merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_merge: true + put api(route, current_user), developers_can_merge: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(CGI.unescape(branch_name)) expect(json_response['protected']).to eq(true) expect(json_response['developers_can_push']).to eq(false) expect(json_response['developers_can_merge']).to eq(true) end it 'protects a single branch and developers can push and merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: true, developers_can_merge: true + put api(route, current_user), developers_can_push: true, developers_can_merge: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(CGI.unescape(branch_name)) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end + + context 'when branch does not exist' do + let(:branch_name) { 'unknown' } + + it_behaves_like '404 response' do + let(:request) { put api(route, current_user) } + let(:message) { '404 Branch Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { put api(route, current_user) } + end + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { put api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { put api(route, guest) } + end + end + + context 'when authenticated', 'as a master' do + let(:current_user) { user } + + context "when a protected branch doesn't already exist" do + it_behaves_like 'repository new protected branch' + + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot.name } + + it_behaves_like 'repository new protected branch' + end + + context 'when branch contains a slash' do + let(:branch_name) { branch_with_slash.name } + + it_behaves_like '404 response' do + let(:request) { put api(route, current_user) } + end + end + + context 'when branch contains an escaped slash' do + let(:branch_name) { CGI.escape(branch_with_slash.name) } + + it_behaves_like 'repository new protected branch' + end + + context 'requesting with the escaped project full path' do + let(:project_id) { CGI.escape(project.full_path) } + + it_behaves_like 'repository new protected branch' + + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot.name } + + it_behaves_like 'repository new protected branch' + end + end + end + + context 'when protected branch already exists' do + before do + project.repository.add_branch(user, protected_branch.name, 'master') + end + + context 'when developers can push and merge' do + let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') } + + it 'updates that a developer cannot push or merge' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: false, developers_can_merge: false + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(protected_branch.name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) + expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) + end + end + + context 'when developers cannot push or merge' do + let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') } + + it 'updates that a developer can push and merge' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: true, developers_can_merge: true + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(protected_branch.name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end + end + end + end + end + + describe 'PUT /projects/:id/repository/branches/:branch/unprotect' do + let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}/unprotect" } + + shared_examples_for 'repository unprotected branch' do + it 'unprotects a single branch' do + put api(route, current_user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq(CGI.unescape(branch_name)) + expect(json_response['protected']).to eq(false) + end + + context 'when branch does not exist' do + let(:branch_name) { 'unknown' } + + it_behaves_like '404 response' do + let(:request) { put api(route, current_user) } + let(:message) { '404 Branch Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { put api(route, current_user) } + end + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { put api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { put api(route, guest) } + end + end + + context 'when authenticated', 'as a master' do + let(:current_user) { user } + + context "when a protected branch doesn't already exist" do + it_behaves_like 'repository unprotected branch' + + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot.name } + + it_behaves_like 'repository unprotected branch' + end + + context 'when branch contains a slash' do + let(:branch_name) { branch_with_slash.name } + + it_behaves_like '404 response' do + let(:request) { put api(route, current_user) } + end + end + + context 'when branch contains an escaped slash' do + let(:branch_name) { CGI.escape(branch_with_slash.name) } + + it_behaves_like 'repository unprotected branch' + end + + context 'requesting with the escaped project full path' do + let(:project_id) { CGI.escape(project.full_path) } + + it_behaves_like 'repository unprotected branch' + + context 'when branch contains a dot' do + let(:branch_name) { branch_with_dot.name } + + it_behaves_like 'repository unprotected branch' + end + end + end + end + end + + describe 'POST /projects/:id/repository/branches' do + let(:route) { "/projects/#{project_id}/repository/branches" } + + shared_examples_for 'repository new branch' do + it 'creates a new branch' do + post api(route, current_user), branch: 'feature1', ref: branch_sha + + expect(response).to have_gitlab_http_status(201) + expect(response).to match_response_schema('public_api/v4/branch') + expect(json_response['name']).to eq('feature1') expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(true) - end - end - - context 'for an existing protected branch' do - before do - project.repository.add_branch(user, protected_branch.name, 'master') end - context "when developers can push and merge" do - let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') } + context 'when repository is disabled' do + include_context 'disabled repository' - it 'updates that a developer cannot push or merge' do - put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), - developers_can_push: false, developers_can_merge: false - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch.name) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - end - - it "doesn't result in 0 access levels when 'developers_can_push' is switched off" do - put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), - developers_can_push: false - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch.name) - expect(protected_branch.reload.push_access_levels.first).to be_present - expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) - end - - it "doesn't result in 0 access levels when 'developers_can_merge' is switched off" do - put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), - developers_can_merge: false - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch.name) - expect(protected_branch.reload.merge_access_levels.first).to be_present - expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) - end - end - - context "when developers cannot push or merge" do - let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') } - - it 'updates that a developer can push and merge' do - put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), - developers_can_push: true, developers_can_merge: true - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch.name) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(true) + it_behaves_like '403 response' do + let(:request) { post api(route, current_user) } end end end - context "multiple API calls" do - it "returns success when `protect` is called twice" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - end - - it "returns success when `protect` is called twice with `developers_can_push` turned on" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(false) - end - - it "returns success when `protect` is called twice with `developers_can_merge` turned on" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(true) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { post api(route) } + let(:message) { '404 Project Not Found' } end end - it "returns a 404 error if branch not found" do - put api("/projects/#{project.id}/repository/branches/unknown/protect", user) - expect(response).to have_http_status(404) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { post api(route, guest) } + end end - it "returns a 403 error if guest" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest) - expect(response).to have_http_status(403) - end - end + context 'when authenticated', 'as a master' do + let(:current_user) { user } - describe "PUT /projects/:id/repository/branches/:branch/unprotect" do - it "unprotects a single branch" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) - expect(response).to have_http_status(200) + context "when a protected branch doesn't already exist" do + it_behaves_like 'repository new branch' - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(false) - end + context 'requesting with the escaped project full path' do + let(:project_id) { CGI.escape(project.full_path) } - it "update branches with dots in branch name" do - put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user) - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_with_dot.name) - expect(json_response['protected']).to eq(false) - end - - it "returns success when unprotect branch" do - put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) - expect(response).to have_http_status(404) - end - - it "returns success when unprotect branch again" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) - put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) - expect(response).to have_http_status(200) - end - end - - describe "POST /projects/:id/repository/branches" do - it "creates a new branch" do - post api("/projects/#{project.id}/repository/branches", user), - branch: 'feature1', - ref: branch_sha - - expect(response).to have_http_status(201) - - expect(json_response['name']).to eq('feature1') - expect(json_response['commit']['id']).to eq(branch_sha) - end - - it "denies for user without push access" do - post api("/projects/#{project.id}/repository/branches", guest), - branch: branch_name, - ref: branch_sha - expect(response).to have_http_status(403) + it_behaves_like 'repository new branch' + end + end end it 'returns 400 if branch name is invalid' do - post api("/projects/#{project.id}/repository/branches", user), - branch: 'new design', - ref: branch_sha - expect(response).to have_http_status(400) + post api(route, user), branch: 'new design', ref: branch_sha + + expect(response).to have_gitlab_http_status(400) expect(json_response['message']).to eq('Branch name is invalid') end it 'returns 400 if branch already exists' do - post api("/projects/#{project.id}/repository/branches", user), - branch: 'new_design1', - ref: branch_sha - expect(response).to have_http_status(201) + post api(route, user), branch: 'new_design1', ref: branch_sha - post api("/projects/#{project.id}/repository/branches", user), - branch: 'new_design1', - ref: branch_sha - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(201) + + post api(route, user), branch: 'new_design1', ref: branch_sha + + expect(response).to have_gitlab_http_status(400) expect(json_response['message']).to eq('Branch already exists') end it 'returns 400 if ref name is invalid' do - post api("/projects/#{project.id}/repository/branches", user), - branch: 'new_design3', - ref: 'foo' - expect(response).to have_http_status(400) + post api(route, user), branch: 'new_design3', ref: 'foo' + + expect(response).to have_gitlab_http_status(400) expect(json_response['message']).to eq('Invalid reference name') end end - describe "DELETE /projects/:id/repository/branches/:branch" do + describe 'DELETE /projects/:id/repository/branches/:branch' do before do allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) end - it "removes branch" do + it 'removes branch' do delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - expect(response).to have_http_status(204) + expect(response).to have_gitlab_http_status(204) end - it "removes a branch with dots in the branch name" do + it 'removes a branch with dots in the branch name' do delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user) - expect(response).to have_http_status(204) + expect(response).to have_gitlab_http_status(204) end it 'returns 404 if branch not exists' do delete api("/projects/#{project.id}/repository/branches/foobar", user) - expect(response).to have_http_status(404) + + expect(response).to have_gitlab_http_status(404) end end - describe "DELETE /projects/:id/repository/merged_branches" do + describe 'DELETE /projects/:id/repository/merged_branches' do before do allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) end @@ -416,13 +509,14 @@ describe API::Branches do it 'returns 202 with json body' do delete api("/projects/#{project.id}/repository/merged_branches", user) - expect(response).to have_http_status(202) + expect(response).to have_gitlab_http_status(202) expect(json_response['message']).to eql('202 Accepted') end it 'returns a 403 error if guest' do delete api("/projects/#{project.id}/repository/merged_branches", guest) - expect(response).to have_http_status(403) + + expect(response).to have_gitlab_http_status(403) end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 656f098aea8..1d7adc6ac45 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -510,7 +510,7 @@ describe API::Groups do describe "POST /groups/:id/projects/:project_id" do let(:project) { create(:empty_project) } - let(:project_path) { project.full_path.gsub('/', '%2F') } + let(:project_path) { CGI.escape(project.full_path) } before(:each) do allow_any_instance_of(Projects::TransferService) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 79e7e1a95df..6ed68fcff09 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -768,7 +768,7 @@ describe API::Projects do dot_user = create(:user, username: 'dot.user') project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace) - get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) + get api("/projects/#{CGI.escape(project.full_path)}", dot_user) expect(response).to have_http_status(200) expect(json_response['name']).to eq(project.name) end diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index 63c5707b2e4..5cdc528e190 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -502,7 +502,7 @@ describe API::V3::Groups do describe "POST /groups/:id/projects/:project_id" do let(:project) { create(:empty_project) } - let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } + let(:project_path) { CGI.escape(project.full_path) } before(:each) do allow_any_instance_of(Projects::TransferService) diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index af44ffa2331..bbfcaab1ea1 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -720,7 +720,7 @@ describe API::V3::Projects do dot_user = create(:user, username: 'dot.user') project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace) - get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) + get v3_api("/projects/#{CGI.escape(project.full_path)}", dot_user) expect(response).to have_http_status(200) expect(json_response['name']).to eq(project.name) end diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb index dff0dfba675..67599f77adb 100644 --- a/spec/support/api/schema_matcher.rb +++ b/spec/support/api/schema_matcher.rb @@ -5,7 +5,14 @@ end RSpec::Matchers.define :match_response_schema do |schema, **options| match do |response| - JSON::Validator.validate!(schema_path(schema), response.body, options) + @errors = JSON::Validator.fully_validate(schema_path(schema), response.body, options) + + @errors.empty? + end + + failure_message do |response| + "didn't match the schema defined by #{schema_path(schema)}" \ + " The validation errors were:\n#{@errors.join("\n")}" end end diff --git a/spec/support/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb similarity index 80% rename from spec/support/api/status_shared_examples.rb rename to spec/support/shared_examples/requests/api/status_shared_examples.rb index 3481749a7f0..226277411d6 100644 --- a/spec/support/api/status_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb @@ -9,7 +9,7 @@ shared_examples_for '400 response' do end it 'returns 400' do - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end end @@ -20,7 +20,7 @@ shared_examples_for '403 response' do end it 'returns 403' do - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end end @@ -32,7 +32,7 @@ shared_examples_for '404 response' do end it 'returns 404' do - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) expect(json_response).to be_an Object if message.present? From 4e3e0dc8d4d742e388372e969324483ab51a3363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 27 Jul 2017 13:01:14 +0200 Subject: [PATCH 142/143] DRY the branches API requirements definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../unreleased/rc-fix-branches-api-endpoint.yml | 2 +- lib/api/api.rb | 3 +++ lib/api/branches.rb | 12 +++++++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/changelogs/unreleased/rc-fix-branches-api-endpoint.yml b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml index 217dfff8c63..a8f49298258 100644 --- a/changelogs/unreleased/rc-fix-branches-api-endpoint.yml +++ b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml @@ -1,5 +1,5 @@ --- title: Fix the /projects/:id/repository/branches endpoint to handle dots in the branch name when the project full patch contains a `/` -merge_request: +merge_request: 13115 author: diff --git a/lib/api/api.rb b/lib/api/api.rb index 3bdafa3edc1..045a0db1842 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -86,6 +86,9 @@ module API helpers ::API::Helpers helpers ::API::Helpers::CommonHelpers + NO_SLASH_URL_PART_REGEX = %r{[^/]+} + PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze + # Keep in alphabetical order mount ::API::AccessRequests mount ::API::AwardEmoji diff --git a/lib/api/branches.rb b/lib/api/branches.rb index cc81f3898ab..9dd60d1833b 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -4,12 +4,14 @@ module API class Branches < Grape::API include PaginationParams + BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX) + before { authorize! :download_code, user_project } params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: { id: %r{[^/]+} } do + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do desc 'Get a project repository branches' do success Entities::RepoBranch end @@ -28,7 +30,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - get ':id/repository/branches/:branch', requirements: { id: %r{[^/]+}, branch: %r{[^/]+} } do + get ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do branch = user_project.repository.find_branch(params[:branch]) not_found!("Branch") unless branch @@ -46,7 +48,7 @@ module API optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch' optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch' end - put ':id/repository/branches/:branch/protect', requirements: { id: %r{[^/]+}, branch: %r{[^/]+} } do + put ':id/repository/branches/:branch/protect', requirements: BRANCH_ENDPOINT_REQUIREMENTS do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -81,7 +83,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - put ':id/repository/branches/:branch/unprotect', requirements: { id: %r{[^/]+}, branch: %r{[^/]+} } do + put ':id/repository/branches/:branch/unprotect', requirements: BRANCH_ENDPOINT_REQUIREMENTS do authorize_admin_project branch = user_project.repository.find_branch(params[:branch]) @@ -118,7 +120,7 @@ module API params do requires :branch, type: String, desc: 'The name of the branch' end - delete ':id/repository/branches/:branch', requirements: { id: %r{[^/]+}, branch: %r{[^/]+} } do + delete ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do authorize_push_project result = DeleteBranchService.new(user_project, current_user) From 9be17322961775ce9ae4aad8cece6db672f059ce Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Thu, 27 Jul 2017 15:44:13 +0200 Subject: [PATCH 143/143] add comment explaining use of shell commands and file operations in the same methods --- lib/gitlab/health_checks/fs_shards_check.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index a4740e9e9b7..9e91c135956 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -77,6 +77,13 @@ module Gitlab storages_paths&.dig(storage_name, 'path') end + # All below test methods use shell commands to perform actions on storage volumes. + # In case a storage volume have connectivity problems causing pure Ruby IO operation to wait indefinitely, + # we can rely on shell commands to be terminated once `timeout` kills them. + # + # However we also fallback to pure Ruby file operations in case a specific shell command is missing + # so we are still able to perform healthchecks and gather metrics from such system. + def delete_test_file(tmp_path) _, status = exec_with_timeout(%W{ rm -f #{tmp_path} }) status.zero?