From 104c8b890dbca25a0d08b2567d003f02953a0fc1 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Sat, 8 Dec 2018 14:12:50 +0000 Subject: [PATCH 01/78] Backport EE GroupSAML origin verification changes --- lib/gitlab/auth/omniauth_identity_linker_base.rb | 6 +++++- .../omniauth_callbacks_controller_spec.rb | 2 +- spec/support/helpers/login_helpers.rb | 13 ++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/auth/omniauth_identity_linker_base.rb b/lib/gitlab/auth/omniauth_identity_linker_base.rb index 253445570f2..c620fc5d6bd 100644 --- a/lib/gitlab/auth/omniauth_identity_linker_base.rb +++ b/lib/gitlab/auth/omniauth_identity_linker_base.rb @@ -12,7 +12,7 @@ module Gitlab end def link - save if identity.new_record? + save if unlinked? end def changed? @@ -35,6 +35,10 @@ module Gitlab @changed = identity.save end + def unlinked? + identity.new_record? + end + # rubocop: disable CodeReuse/ActiveRecord def identity @identity ||= current_user.identities diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 59463462e5a..f995867c5e1 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -170,7 +170,7 @@ describe OmniauthCallbacksController, type: :controller do before do stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config] }) - mock_auth_hash('saml', 'my-uid', user.email, mock_saml_response) + mock_auth_hash_with_saml_xml('saml', 'my-uid', user.email, mock_saml_response) request.env["devise.mapping"] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] post :saml, params: { SAMLResponse: mock_saml_response } diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 87cfb6c04dc..c709f160bff 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -47,7 +47,7 @@ module LoginHelpers end def gitlab_sign_in_via(provider, user, uid, saml_response = nil) - mock_auth_hash(provider, uid, user.email, saml_response) + mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response) visit new_user_session_path click_link provider end @@ -87,7 +87,12 @@ module LoginHelpers click_link "oauth-login-#{provider}" end - def mock_auth_hash(provider, uid, email, saml_response = nil) + def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response) + response_object = { document: saml_xml(saml_response) } + mock_auth_hash(provider, uid, email, response_object: response_object) + end + + def mock_auth_hash(provider, uid, email, response_object: nil) # The mock_auth configuration allows you to set per-provider (or default) # authentication hashes to return during integration testing. OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({ @@ -110,9 +115,7 @@ module LoginHelpers image: 'mock_user_thumbnail_url' } }, - response_object: { - document: saml_xml(saml_response) - } + response_object: response_object } }) Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] -- GitLab From 276147c1f6c54ae87941953ec62d0d674c12f462 Mon Sep 17 00:00:00 2001 From: Patrick Bajao Date: Fri, 25 Jan 2019 15:44:50 +0800 Subject: [PATCH 02/78] Respond with 403 when non-member requests for private MRs --- lib/api/commits.rb | 2 ++ spec/requests/api/commits_spec.rb | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 9d23daafe95..41cb3e17af8 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -318,6 +318,8 @@ module API use :pagination end get ':id/repository/commits/:sha/merge_requests', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do + authorize! :read_merge_request, user_project + commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 6b9bc6eda6a..c24e17fda3f 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -1430,8 +1430,8 @@ describe API::Commits do end describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do - let!(:project) { create(:project, :repository, :private) } - let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } + let(:project) { create(:project, :repository, :private) } + let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } let(:commit) { merged_mr.merge_request_diff.commits.last } it 'returns the correct merge request' do @@ -1456,5 +1456,16 @@ describe API::Commits do expect(response).to have_gitlab_http_status(404) end + + context 'public project' do + let(:project) { create(:project, :repository, :public, :merge_requests_private) } + let(:non_member) { create(:user) } + + it 'responds 403 when only members are allowed to read merge requests' do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", non_member) + + expect(response).to have_gitlab_http_status(403) + end + end end end -- GitLab From 65723a2cb60f46681a766f34102df700ef5b7573 Mon Sep 17 00:00:00 2001 From: Patrick Bajao Date: Fri, 25 Jan 2019 17:22:48 +0800 Subject: [PATCH 03/78] Modify MergeRequestsFinder to allow filtering by commit --- app/controllers/projects/commit_controller.rb | 6 ++++- app/finders/merge_requests_finder.rb | 9 ++++++- lib/api/commits.rb | 8 +++++- spec/finders/merge_requests_finder_spec.rb | 26 ++++++++++++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 32fc5140366..fbad56cf6b3 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -65,7 +65,11 @@ class Projects::CommitController < Projects::ApplicationController # rubocop: enable CodeReuse/ActiveRecord def merge_requests - @merge_requests = @commit.merge_requests.map do |mr| + @merge_requests = MergeRequestsFinder.new( + current_user, + project_id: @project.id, + commit_sha: @commit.sha + ).execute.map do |mr| { iid: mr.iid, path: merge_request_path(mr), title: mr.title } end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index e190d5d90c9..bf01f05a9e9 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -36,13 +36,20 @@ class MergeRequestsFinder < IssuableFinder end def filter_items(_items) - items = by_source_branch(super) + items = by_commit(super) + items = by_source_branch(items) items = by_wip(items) by_target_branch(items) end private + def by_commit(items) + return items unless params[:commit_sha].presence + + items.by_commit_sha(params[:commit_sha]) + end + def source_branch @source_branch ||= params[:source_branch].presence end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 41cb3e17af8..be682982897 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -323,7 +323,13 @@ module API commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit - present paginate(commit.merge_requests), with: Entities::MergeRequestBasic + commit_merge_requests = MergeRequestsFinder.new( + current_user, + project_id: user_project.id, + commit_sha: commit.sha + ).execute + + present paginate(commit_merge_requests), with: Entities::MergeRequestBasic end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 107da08a0a9..79f854cdb96 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -31,7 +31,7 @@ describe MergeRequestsFinder do p end end - let(:project4) { create_project_without_n_plus_1(group: subgroup) } + let(:project4) { create_project_without_n_plus_1(:repository, group: subgroup) } let(:project5) { create_project_without_n_plus_1(group: subgroup) } let(:project6) { create_project_without_n_plus_1(group: subgroup) } @@ -68,6 +68,15 @@ describe MergeRequestsFinder do expect(merge_requests.size).to eq(2) end + it 'filters by commit sha' do + merge_requests = described_class.new( + user, + commit_sha: merge_request5.merge_request_diff.last_commit_sha + ).execute + + expect(merge_requests).to contain_exactly(merge_request5) + end + context 'filtering by group' do it 'includes all merge requests when user has access' do params = { group_id: group.id } @@ -269,6 +278,21 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) end end + + context 'when project restricts merge requests' do + let(:non_member) { create(:user) } + let(:project) { create(:project, :repository, :public, :merge_requests_private) } + let!(:merge_request) { create(:merge_request, source_project: project) } + + it "returns nothing to to non members" do + merge_requests = described_class.new( + non_member, + project_id: project.id + ).execute + + expect(merge_requests).to be_empty + end + end end describe '#row_count', :request_store do -- GitLab From 325527e6ca7635aeeea8e0beb7523c3892e21bf6 Mon Sep 17 00:00:00 2001 From: Patrick Bajao Date: Mon, 28 Jan 2019 14:16:58 +0800 Subject: [PATCH 04/78] Add changelog for security fix --- changelogs/unreleased/security-commit-private-related-mr.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/security-commit-private-related-mr.yml diff --git a/changelogs/unreleased/security-commit-private-related-mr.yml b/changelogs/unreleased/security-commit-private-related-mr.yml new file mode 100644 index 00000000000..c4de200b0d8 --- /dev/null +++ b/changelogs/unreleased/security-commit-private-related-mr.yml @@ -0,0 +1,5 @@ +--- +title: Don't allow non-members to see private related MRs. +merge_request: +author: +type: security -- GitLab From d5c858cd4032b3bf37c6fbe47340ccea825503bc Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Fri, 8 Feb 2019 16:11:37 +0000 Subject: [PATCH 05/78] Prevent Releases links API to leak tag existance --- changelogs/unreleased/security-tags-oracle.yml | 5 +++++ lib/api/release/links.rb | 2 ++ spec/requests/api/release/links_spec.rb | 16 ++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 changelogs/unreleased/security-tags-oracle.yml diff --git a/changelogs/unreleased/security-tags-oracle.yml b/changelogs/unreleased/security-tags-oracle.yml new file mode 100644 index 00000000000..eb8ad6f646c --- /dev/null +++ b/changelogs/unreleased/security-tags-oracle.yml @@ -0,0 +1,5 @@ +--- +title: Prevent releases links API to leak tag existance +merge_request: +author: +type: security diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index e3072684ef7..5d1b40e3bff 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -8,6 +8,8 @@ module API RELEASE_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS .merge(tag_name: API::NO_SLASH_URL_PART_REGEX) + before { authorize! :read_release, user_project } + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index ba948e37e2f..3a59052bb29 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -73,6 +73,22 @@ describe API::Release::Links do expect(response).to have_gitlab_http_status(:ok) end end + + context 'when project is public and the repository is private' do + let(:project) { create(:project, :repository, :public, :repository_private) } + + it_behaves_like '403 response' do + let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) } + end + + context 'when the release does not exists' do + let!(:release) { } + + it_behaves_like '403 response' do + let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) } + end + end + end end end -- GitLab From 5dc047dc72c08a64aaf4f4a0c9fe0fba2742b905 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu Date: Mon, 11 Feb 2019 18:51:53 +0800 Subject: [PATCH 06/78] Disable board policies when issues are disabled Board list policies are also included --- app/policies/project_policy.rb | 2 ++ .../security-2798-fix-boards-policy.yml | 5 +++++ spec/policies/project_policy_spec.rb | 20 +++++++++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/security-2798-fix-boards-policy.yml diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index cadbc5ae009..a8270442ea9 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -299,6 +299,8 @@ class ProjectPolicy < BasePolicy rule { issues_disabled }.policy do prevent(*create_read_update_admin_destroy(:issue)) + prevent(*create_read_update_admin_destroy(:board)) + prevent(*create_read_update_admin_destroy(:list)) end rule { merge_requests_disabled | repository_disabled }.policy do diff --git a/changelogs/unreleased/security-2798-fix-boards-policy.yml b/changelogs/unreleased/security-2798-fix-boards-policy.yml new file mode 100644 index 00000000000..10e8ac3a787 --- /dev/null +++ b/changelogs/unreleased/security-2798-fix-boards-policy.yml @@ -0,0 +1,5 @@ +--- +title: Disable issue boards API when issues are disabled +merge_request: +author: +type: security diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 93a468f585b..f8d581ef38f 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -130,22 +130,26 @@ describe ProjectPolicy do subject { described_class.new(owner, project) } context 'when the feature is disabled' do - it 'does not include the issues permissions' do + before do project.issues_enabled = false project.save! + end + it 'does not include the issues permissions' do expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue end - end - context 'when the feature is disabled and external tracker configured' do - it 'does not include the issues permissions' do - create(:jira_service, project: project) + it 'disables boards and lists permissions' do + expect_disallowed :read_board, :create_board, :update_board, :admin_board + expect_disallowed :read_list, :create_list, :update_list, :admin_list + end - project.issues_enabled = false - project.save! + context 'when external tracker configured' do + it 'does not include the issues permissions' do + create(:jira_service, project: project) - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + end end end end -- GitLab From a9291f15ea10e3cfc94282ffb4e0969e9d4175eb Mon Sep 17 00:00:00 2001 From: Mark Chao Date: Wed, 13 Feb 2019 16:23:58 +0800 Subject: [PATCH 07/78] Align spec with actual usage Currently we pass temp file path to FileMover --- spec/uploaders/file_mover_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index de29d0c943f..a28d7445b1c 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe FileMover do let(:filename) { 'banana_sample.gif' } - let(:file) { fixture_file_upload(File.join('spec', 'fixtures', filename)) } let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) } let(:temp_description) do @@ -12,7 +11,7 @@ describe FileMover do let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) } let(:snippet) { create(:personal_snippet, description: temp_description) } - subject { described_class.new(file_path, snippet).execute } + subject { described_class.new(temp_file_path, snippet).execute } describe '#execute' do before do -- GitLab From 30ab6ee416783cd9481085f021603383eeb4f317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Mon, 14 Jan 2019 11:46:39 +0100 Subject: [PATCH 08/78] Check issue milestone availability Add project when creating milestone in specs We validate milestone is from the same project/parent group as issuable -> we need to set project in specs correctly Improve methods names and specs organization --- app/models/concerns/issuable.rb | 11 +++ app/models/merge_request.rb | 7 +- app/services/issuable_base_service.rb | 6 ++ app/services/issues/build_service.rb | 4 +- app/services/merge_requests/build_service.rb | 1 + .../51971-milestones-visibility.yml | 5 + .../dashboard/milestones_controller_spec.rb | 2 +- spec/features/issues_spec.rb | 4 +- .../user_lists_merge_requests_spec.rb | 4 +- spec/models/concerns/issuable_spec.rb | 97 +++++++++++++++++-- spec/models/issue/metrics_spec.rb | 6 +- spec/models/milestone_spec.rb | 8 +- spec/requests/api/issues_spec.rb | 2 +- .../common_system_notes_service_spec.rb | 4 +- spec/services/issues/build_service_spec.rb | 76 +++++++-------- spec/services/issues/update_service_spec.rb | 6 +- .../merge_requests/build_service_spec.rb | 9 ++ .../merge_requests/update_service_spec.rb | 6 +- .../issuable_shared_examples.rb | 2 +- 19 files changed, 187 insertions(+), 73 deletions(-) create mode 100644 changelogs/unreleased/51971-milestones-visibility.yml diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0a77fbeba08..8918dc8f417 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -74,6 +74,7 @@ module Issuable validates :author, presence: true validates :title, presence: true, length: { maximum: 255 } + validate :milestone_is_valid scope :authored, ->(user) { where(author_id: user) } scope :recent, -> { reorder(id: :desc) } @@ -117,6 +118,16 @@ module Issuable def has_multiple_assignees? assignees.count > 1 end + + def milestone_available? + project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group) + end + + private + + def milestone_is_valid + errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available? + end end class_methods do diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2035bffd829..a551e3e387d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -189,6 +189,9 @@ class MergeRequest < ActiveRecord::Base after_save :keep_around_commit + alias_attribute :project, :target_project + alias_attribute :project_id, :target_project_id + def self.reference_prefix '!' end @@ -837,10 +840,6 @@ class MergeRequest < ActiveRecord::Base target_project != source_project end - def project - target_project - end - # If the merge request closes any issues, save this information in the # `MergeRequestsClosingIssues` model. This is a performance optimization. # Calculating this information for a number of merge requests requires diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index ef991eaf234..1e1f2fbd08e 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -387,4 +387,10 @@ class IssuableBaseService < BaseService def parent project end + + # we need to check this because milestone from milestone_id param is displayed on "new" page + # where private project milestone could leak without this check + def ensure_milestone_available(issuable) + issuable.milestone_id = nil unless issuable.milestone_available? + end end diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index 52b45f1b2ce..77724e78972 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -6,7 +6,9 @@ module Issues def execute filter_resolve_discussion_params - @issue = project.issues.new(issue_params) + @issue = project.issues.new(issue_params).tap do |issue| + ensure_milestone_available(issue) + end end def issue_params_with_info_from_discussions diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 48419da98ad..109c964e577 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -19,6 +19,7 @@ module MergeRequests merge_request.target_project = find_target_project merge_request.target_branch = find_target_branch merge_request.can_be_created = projects_and_branches_valid? + ensure_milestone_available(merge_request) # compare branches only if branches are valid, otherwise # compare_branches may raise an error diff --git a/changelogs/unreleased/51971-milestones-visibility.yml b/changelogs/unreleased/51971-milestones-visibility.yml new file mode 100644 index 00000000000..818f0071e6c --- /dev/null +++ b/changelogs/unreleased/51971-milestones-visibility.yml @@ -0,0 +1,5 @@ +--- +title: Check if desired milestone for an issue is available +merge_request: +author: +type: security diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 8b176e07bc8..ddf33ebad16 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -15,7 +15,7 @@ describe Dashboard::MilestonesController do ) end let(:issue) { create(:issue, project: project, milestone: project_milestone) } - let(:group_issue) { create(:issue, milestone: group_milestone) } + let(:group_issue) { create(:issue, milestone: group_milestone, project: create(:project, group: group)) } let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) } let!(:group_label) { create(:group_label, group: group, title: 'Group Issue Label', issues: [group_issue]) } diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 406e80e91aa..9bc340ed4bb 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -233,8 +233,8 @@ describe 'Issues' do created_at: Time.now - (index * 60)) end end - let(:newer_due_milestone) { create(:milestone, due_date: '2013-12-11') } - let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') } + let(:newer_due_milestone) { create(:milestone, project: project, due_date: '2013-12-11') } + let(:later_due_milestone) { create(:milestone, project: project, due_date: '2013-12-12') } it 'sorts by newest' do visit project_issues_path(project, sort: sort_value_created_date) diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index ef7ae490b0f..c691011b9ca 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -13,7 +13,7 @@ describe 'Merge requests > User lists merge requests' do source_project: project, source_branch: 'fix', assignee: user, - milestone: create(:milestone, due_date: '2013-12-11'), + milestone: create(:milestone, project: project, due_date: '2013-12-11'), created_at: 1.minute.ago, updated_at: 1.minute.ago) create(:merge_request, @@ -21,7 +21,7 @@ describe 'Merge requests > User lists merge requests' do source_project: project, source_branch: 'markdown', assignee: user, - milestone: create(:milestone, due_date: '2013-12-12'), + milestone: create(:milestone, project: project, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) create(:merge_request, diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 41159348e04..72c6161424b 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -32,17 +32,56 @@ describe Issuable do end describe "Validation" do - subject { build(:issue) } + context 'general validations' do + subject { build(:issue) } - before do - allow(InternalId).to receive(:generate_next).and_return(nil) + before do + allow(InternalId).to receive(:generate_next).and_return(nil) + end + + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:iid) } + it { is_expected.to validate_presence_of(:author) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_length_of(:title).is_at_most(255) } end - it { is_expected.to validate_presence_of(:project) } - it { is_expected.to validate_presence_of(:iid) } - it { is_expected.to validate_presence_of(:author) } - it { is_expected.to validate_presence_of(:title) } - it { is_expected.to validate_length_of(:title).is_at_most(255) } + describe 'milestone' do + let(:project) { create(:project) } + let(:milestone_id) { create(:milestone, project: project).id } + let(:params) do + { + title: 'something', + project: project, + author: build(:user), + milestone_id: milestone_id + } + end + + subject { issuable_class.new(params) } + + context 'with correct params' do + it { is_expected.to be_valid } + end + + context 'with empty string milestone' do + let(:milestone_id) { '' } + + it { is_expected.to be_valid } + end + + context 'with nil milestone id' do + let(:milestone_id) { nil } + + it { is_expected.to be_valid } + end + + context 'with a milestone id from another project' do + let(:milestone_id) { create(:milestone).id } + + it { is_expected.to be_invalid } + end + end end describe "Scope" do @@ -66,6 +105,48 @@ describe Issuable do end end + describe '#milestone_available?' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:issue) { create(:issue, project: project) } + + def build_issuable(milestone_id) + issuable_class.new(project: project, milestone_id: milestone_id) + end + + it 'returns true with a milestone from the issue project' do + milestone = create(:milestone, project: project) + + expect(build_issuable(milestone.id).milestone_available?).to be_truthy + end + + it 'returns true with a milestone from the issue project group' do + milestone = create(:milestone, group: group) + + expect(build_issuable(milestone.id).milestone_available?).to be_truthy + end + + it 'returns true with a milestone from the the parent of the issue project group', :nested_groups do + parent = create(:group) + group.update(parent: parent) + milestone = create(:milestone, group: parent) + + expect(build_issuable(milestone.id).milestone_available?).to be_truthy + end + + it 'returns false with a milestone from another project' do + milestone = create(:milestone) + + expect(build_issuable(milestone.id).milestone_available?).to be_falsey + end + + it 'returns false with a milestone from another group' do + milestone = create(:milestone, group: create(:group)) + + expect(build_issuable(milestone.id).milestone_available?).to be_falsey + end + end + describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") } let!(:searchable_issue2) { create(:issue, title: 'Aw') } diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb index 1bf0ecb98ad..b7291eebe64 100644 --- a/spec/models/issue/metrics_spec.rb +++ b/spec/models/issue/metrics_spec.rb @@ -9,7 +9,7 @@ describe Issue::Metrics do context "milestones" do it "records the first time an issue is associated with a milestone" do time = Time.now - Timecop.freeze(time) { subject.update(milestone: create(:milestone)) } + Timecop.freeze(time) { subject.update(milestone: create(:milestone, project: project)) } metrics = subject.metrics expect(metrics).to be_present @@ -18,9 +18,9 @@ describe Issue::Metrics do it "does not record the second time an issue is associated with a milestone" do time = Time.now - Timecop.freeze(time) { subject.update(milestone: create(:milestone)) } + Timecop.freeze(time) { subject.update(milestone: create(:milestone, project: project)) } Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) } - Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone)) } + Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone, project: project)) } metrics = subject.metrics expect(metrics).to be_present diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index af7e3d3a6c9..77b7042424c 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -182,7 +182,7 @@ describe Milestone do describe '#total_items_count' do before do create :closed_issue, milestone: milestone, project: project - create :merge_request, milestone: milestone + create :merge_request, milestone: milestone, source_project: project end it 'returns total count of issues and merge requests assigned to milestone' do @@ -192,10 +192,10 @@ describe Milestone do describe '#can_be_closed?' do before do - milestone = create :milestone - create :closed_issue, milestone: milestone + milestone = create :milestone, project: project + create :closed_issue, milestone: milestone, project: project - create :issue + create :issue, project: project end it 'returns true if milestone active and all nested issues closed' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 04908378a24..46cd3ec88e1 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -49,7 +49,7 @@ describe API::Issues do create(:label, title: 'label', color: '#FFAABB', project: project) end let!(:label_link) { create(:label_link, label: label, target: issue) } - set(:milestone) { create(:milestone, title: '1.0.0', project: project) } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } set(:empty_milestone) do create(:milestone, title: '2.0.0', project: project) end diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb index fa5d5ebac5c..0edc9016c96 100644 --- a/spec/services/issuable/common_system_notes_service_spec.rb +++ b/spec/services/issuable/common_system_notes_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Issuable::CommonSystemNotesService do let(:user) { create(:user) } let(:project) { create(:project) } - let(:issuable) { create(:issue) } + let(:issuable) { create(:issue, project: project) } context 'on issuable update' do it_behaves_like 'system note creation', { title: 'New title' }, 'changed title' @@ -70,7 +70,7 @@ describe Issuable::CommonSystemNotesService do end context 'on issuable create' do - let(:issuable) { build(:issue) } + let(:issuable) { build(:issue, project: project) } subject { described_class.new(project, user).execute(issuable, old_labels: [], is_update: false) } diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 248e7d5a389..86e58fe06b9 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -8,29 +8,29 @@ describe Issues::BuildService do project.add_developer(user) end + def build_issue(issue_params = {}) + described_class.new(project, user, issue_params).execute + end + context 'for a single discussion' do describe '#execute' do let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) } let(:discussion) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done").to_discussion } - let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) } - it 'references the noteable title in the issue title' do - issue = service.execute + subject { build_issue(merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) } - expect(issue.title).to include('Hello world') + it 'references the noteable title in the issue title' do + expect(subject.title).to include('Hello world') end it 'adds the note content to the description' do - issue = service.execute - - expect(issue.description).to include('Almost done') + expect(subject.description).to include('Almost done') end end end context 'for discussions in a merge request' do let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) } - let(:issue) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute } describe '#items_for_discussions' do it 'has an item for each discussion' do @@ -66,28 +66,30 @@ describe Issues::BuildService do end describe '#execute' do - it 'has the merge request reference in the title' do - expect(issue.title).to include(merge_request.title) - end + let(:base_params) { { merge_request_to_resolve_discussions_of: merge_request.iid } } - it 'has the reference of the merge request in the description' do - expect(issue.description).to include(merge_request.to_reference) + context 'without additional params' do + subject { build_issue(base_params) } + + it 'has the merge request reference in the title' do + expect(subject.title).to include(merge_request.title) + end + + it 'has the reference of the merge request in the description' do + expect(subject.description).to include(merge_request.to_reference) + end end - it 'does not assign title when a title was given' do - issue = described_class.new(project, user, - merge_request_to_resolve_discussions_of: merge_request, - title: 'What an issue').execute + it 'uses provided title if title param given' do + issue = build_issue(base_params.merge(title: 'What an issue')) expect(issue.title).to eq('What an issue') end - it 'does not assign description when a description was given' do - issue = described_class.new(project, user, - merge_request_to_resolve_discussions_of: merge_request, - description: 'Fix at your earliest conveignance').execute + it 'uses provided description if description param given' do + issue = build_issue(base_params.merge(description: 'Fix at your earliest convenience')) - expect(issue.description).to eq('Fix at your earliest conveignance') + expect(issue.description).to eq('Fix at your earliest convenience') end describe 'with multiple discussions' do @@ -96,20 +98,20 @@ describe Issues::BuildService do it 'mentions all the authors in the description' do authors = merge_request.resolvable_discussions.map(&:author) - expect(issue.description).to include(*authors.map(&:to_reference)) + expect(build_issue(base_params).description).to include(*authors.map(&:to_reference)) end it 'has a link for each unresolved discussion in the description' do notes = merge_request.resolvable_discussions.map(&:first_note) links = notes.map { |note| Gitlab::UrlBuilder.build(note) } - expect(issue.description).to include(*links) + expect(build_issue(base_params).description).to include(*links) end it 'mentions additional notes' do create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, in_reply_to: diff_note) - expect(issue.description).to include('(+2 comments)') + expect(build_issue(base_params).description).to include('(+2 comments)') end end end @@ -120,7 +122,7 @@ describe Issues::BuildService do describe '#execute' do it 'mentions the merge request in the description' do - issue = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute + issue = build_issue(merge_request_to_resolve_discussions_of: merge_request.iid) expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}") end @@ -128,20 +130,18 @@ describe Issues::BuildService do end describe '#execute' do - let(:milestone) { create(:milestone, project: project) } - it 'builds a new issues with given params' do - issue = described_class.new( - project, - user, - title: 'Issue #1', - description: 'Issue description', - milestone_id: milestone.id - ).execute - - expect(issue.title).to eq('Issue #1') - expect(issue.description).to eq('Issue description') + milestone = create(:milestone, project: project) + issue = build_issue(milestone_id: milestone.id) + expect(issue.milestone).to eq(milestone) end + + it 'sets milestone to nil if it is not available for the project' do + milestone = create(:milestone, project: create(:project)) + issue = build_issue(milestone_id: milestone.id) + + expect(issue.milestone).to be_nil + end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 931e47d3a77..f1684209729 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -356,7 +356,7 @@ describe Issues::UpdateService, :mailer do it_behaves_like 'system notes for milestones' it 'sends notifications for subscribers of changed milestone' do - issue.milestone = create(:milestone) + issue.milestone = create(:milestone, project: project) issue.save @@ -380,7 +380,7 @@ describe Issues::UpdateService, :mailer do end it 'marks todos as done' do - update_issue(milestone: create(:milestone)) + update_issue(milestone: create(:milestone, project: project)) expect(todo.reload.done?).to eq true end @@ -389,7 +389,7 @@ describe Issues::UpdateService, :mailer do it 'sends notifications for subscribers of changed milestone' do perform_enqueued_jobs do - update_issue(milestone: create(:milestone)) + update_issue(milestone: create(:milestone, project: project)) end should_email(subscriber) diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 536d0d345a4..057e8137a4e 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -229,6 +229,15 @@ describe MergeRequests::BuildService do end end end + + context 'when a milestone is from another project' do + let(:milestone) { create(:milestone, project: create(:project)) } + let(:milestone_id) { milestone.id } + + it 'sets milestone to nil' do + expect(merge_request.milestone).to be_nil + end + end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 20580bf14b9..8e367db031c 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -328,7 +328,7 @@ describe MergeRequests::UpdateService, :mailer do it_behaves_like 'system notes for milestones' it 'sends notifications for subscribers of changed milestone' do - merge_request.milestone = create(:milestone) + merge_request.milestone = create(:milestone, project: project) merge_request.save @@ -352,7 +352,7 @@ describe MergeRequests::UpdateService, :mailer do end it 'marks pending todos as done' do - update_merge_request({ milestone: create(:milestone) }) + update_merge_request({ milestone: create(:milestone, project: project) }) expect(pending_todo.reload).to be_done end @@ -361,7 +361,7 @@ describe MergeRequests::UpdateService, :mailer do it 'sends notifications for subscribers of changed milestone' do perform_enqueued_jobs do - update_merge_request(milestone: create(:milestone)) + update_merge_request(milestone: create(:milestone, project: project)) end should_email(subscriber) diff --git a/spec/support/shared_examples/issuable_shared_examples.rb b/spec/support/shared_examples/issuable_shared_examples.rb index c3d40c5b231..d97b21f71cd 100644 --- a/spec/support/shared_examples/issuable_shared_examples.rb +++ b/spec/support/shared_examples/issuable_shared_examples.rb @@ -31,7 +31,7 @@ shared_examples 'system notes for milestones' do context 'project milestones' do it 'creates a system note' do expect do - update_issuable(milestone: create(:milestone)) + update_issuable(milestone: create(:milestone, project: project)) end.to change { Note.system.count }.by(1) end end -- GitLab From 8ad127083ef28cfea389ec00a6f7ebf39e15c431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Mon, 11 Feb 2019 11:31:30 +0100 Subject: [PATCH 09/78] Show only MRs visible to user on milestone detail --- app/controllers/concerns/milestone_actions.rb | 2 +- app/models/concerns/milestoneish.rb | 11 ++++- .../security-2797-milestone-mrs.yml | 5 ++ spec/models/concerns/milestoneish_spec.rb | 47 ++++++++++++++++++- 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/security-2797-milestone-mrs.yml diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index eccbe35577b..c0c0160a827 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -8,7 +8,7 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_merge_requests_tab", { - merge_requests: @milestone.sorted_merge_requests, # rubocop:disable Gitlab/ModuleWithInstanceVariables + merge_requests: @milestone.sorted_merge_requests(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables show_project_name: true }) end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 055ffe04646..39372c4f68b 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -46,12 +46,19 @@ module Milestoneish end end + def merge_requests_visible_to_user(user) + memoize_per_user(user, :merge_requests_visible_to_user) do + MergeRequestsFinder.new(user, {}) + .execute.where(milestone_id: milestoneish_id) + end + end + def sorted_issues(user) issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority') end - def sorted_merge_requests - merge_requests.sort_by_attribute('label_priority') + def sorted_merge_requests(user) + merge_requests_visible_to_user(user).sort_by_attribute('label_priority') end def upcoming? diff --git a/changelogs/unreleased/security-2797-milestone-mrs.yml b/changelogs/unreleased/security-2797-milestone-mrs.yml new file mode 100644 index 00000000000..5bb104ec403 --- /dev/null +++ b/changelogs/unreleased/security-2797-milestone-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Show only merge requests visible to user on milestone detail page +merge_request: +author: +type: security diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index 87bf731340f..4647eecbdef 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -48,7 +48,7 @@ describe Milestone, 'Milestoneish' do merge_request_2 = create(:labeled_merge_request, labels: [label_1], source_project: project, source_branch: 'branch_2', milestone: milestone) merge_request_3 = create(:labeled_merge_request, labels: [label_3], source_project: project, source_branch: 'branch_3', milestone: milestone) - merge_requests = milestone.sorted_merge_requests + merge_requests = milestone.sorted_merge_requests(member) expect(merge_requests.first).to eq(merge_request_2) expect(merge_requests.second).to eq(merge_request_1) @@ -56,6 +56,51 @@ describe Milestone, 'Milestoneish' do end end + describe '#merge_requests_visible_to_user' do + let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + + context 'when project is private' do + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it 'does not return any merge request for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to be_empty + end + + it 'returns milestone merge requests for a member' do + merge_requests = milestone.merge_requests_visible_to_user(member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + + context 'when project is public' do + context 'when merge requests are available to anyone' do + it 'returns milestone merge requests for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + + context 'when merge requests are available to project members' do + before do + project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it 'does not return any merge request for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to be_empty + end + + it 'returns milestone merge requests for a member' do + merge_requests = milestone.merge_requests_visible_to_user(member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + end + end + describe '#closed_items_count' do it 'does not count confidential issues for non project members' do expect(milestone.closed_items_count(non_member)).to eq 2 -- GitLab From 618b87448e9167f39d8216d1100733cc0fbf020b Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Thu, 31 Jan 2019 17:21:35 +1300 Subject: [PATCH 10/78] Prevent leaking of private repo data through API default_branch, statistics and config_ci_path are now only exposed if the user has permissions to the repository. --- lib/api/entities.rb | 9 +++-- lib/api/environments.rb | 8 ++-- lib/api/projects.rb | 24 +++++++----- spec/requests/api/projects_spec.rb | 59 +++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 20 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 27da2c2e5ed..46cd4841e2d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -156,7 +156,7 @@ module API class BasicProjectDetails < ProjectIdentity include ::API::ProjectsRelationBuilder - expose :default_branch + expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 expose :tag_list do |project| # project.tags.order(:name).pluck(:name) is the most suitable option @@ -261,7 +261,7 @@ module API expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds, as: :public_jobs - expose :ci_config_path + expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links, options) end @@ -270,8 +270,9 @@ module API expose :only_allow_merge_if_all_discussions_are_resolved expose :printing_merge_request_link_enabled expose :merge_method - - expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics + expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { + options[:statistics] && Ability.allowed?(options[:current_user], :download_code, project) + } # rubocop: disable CodeReuse/ActiveRecord def self.preload_relation(projects_relation, options = {}) diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 0278c6c54a5..5b0f3b914cb 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -22,7 +22,7 @@ module API get ':id/environments' do authorize! :read_environment, user_project - present paginate(user_project.environments), with: Entities::Environment + present paginate(user_project.environments), with: Entities::Environment, current_user: current_user end desc 'Creates a new environment' do @@ -40,7 +40,7 @@ module API environment = user_project.environments.create(declared_params) if environment.persisted? - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user else render_validation_error!(environment) end @@ -63,7 +63,7 @@ module API update_params = declared_params(include_missing: false).extract!(:name, :external_url) if environment.update(update_params) - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user else render_validation_error!(environment) end @@ -99,7 +99,7 @@ module API environment.stop_with_action!(current_user) status 200 - present environment, with: Entities::Environment + present environment, with: Entities::Environment, current_user: current_user end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6a93ef9f3ad..2325fc96a67 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -184,7 +184,8 @@ module API if project.saved? present project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) + user_can_admin_project: can?(current_user, :admin_project, project), + current_user: current_user else if project.errors[:limit_reached].present? error!(project.errors[:limit_reached], 403) @@ -217,7 +218,8 @@ module API if project.saved? present project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) + user_can_admin_project: can?(current_user, :admin_project, project), + current_user: current_user else render_validation_error!(project) end @@ -279,7 +281,8 @@ module API conflict!(forked_project.errors.messages) else present forked_project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, forked_project) + user_can_admin_project: can?(current_user, :admin_project, forked_project), + current_user: current_user end end @@ -328,7 +331,8 @@ module API if result[:status] == :success present user_project, with: Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, user_project) + user_can_admin_project: can?(current_user, :admin_project, user_project), + current_user: current_user else render_validation_error!(user_project) end @@ -342,7 +346,7 @@ module API ::Projects::UpdateService.new(user_project, current_user, archived: true).execute - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end desc 'Unarchive a project' do @@ -353,7 +357,7 @@ module API ::Projects::UpdateService.new(@project, current_user, archived: false).execute - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end desc 'Star a project' do @@ -366,7 +370,7 @@ module API current_user.toggle_star(user_project) user_project.reload - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user end end @@ -378,7 +382,7 @@ module API current_user.toggle_star(user_project) user_project.reload - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user else not_modified! end @@ -414,7 +418,7 @@ module API result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project) if result - present user_project.reload, with: Entities::Project + present user_project.reload, with: Entities::Project, current_user: current_user else render_api_error!("Project already forked", 409) if user_project.forked? end @@ -520,7 +524,7 @@ module API result = ::Projects::TransferService.new(user_project, current_user).execute(namespace) if result - present user_project, with: Entities::Project + present user_project, with: Entities::Project, current_user: current_user else render_api_error!("Failed to transfer project #{user_project.errors.messages}", 400) end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index cfa7a1a31a3..1c05be8de70 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -110,6 +110,7 @@ describe API::Projects do end let!(:public_project) { create(:project, :public, name: 'public_project') } + before do project project2 @@ -942,8 +943,16 @@ describe API::Projects do describe 'GET /projects/:id' do context 'when unauthenticated' do - it 'returns the public projects' do - public_project = create(:project, :public) + it 'does not return private projects' do + private_project = create(:project, :private) + + get api("/projects/#{private_project.id}") + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns public projects' do + public_project = create(:project, :repository, :public) get api("/projects/#{public_project.id}") @@ -951,8 +960,34 @@ describe API::Projects do expect(json_response['id']).to eq(public_project.id) expect(json_response['description']).to eq(public_project.description) expect(json_response['default_branch']).to eq(public_project.default_branch) + expect(json_response['ci_config_path']).to eq(public_project.ci_config_path) expect(json_response.keys).not_to include('permissions') end + + context 'and the project has a private repository' do + let(:project) { create(:project, :repository, :public, :repository_private) } + let(:protected_attributes) { %w(default_branch ci_config_path) } + + it 'hides protected attributes of private repositories if user is not a member' do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + protected_attributes.each do |attribute| + expect(json_response.keys).not_to include(attribute) + end + end + + it 'exposes protected attributes of private repositories if user is a member' do + project.add_developer(user) + + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + protected_attributes.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + end end context 'when authenticated' do @@ -1104,6 +1139,26 @@ describe API::Projects do expect(json_response).to include 'statistics' end + context "and the project has a private repository" do + let(:project) { create(:project, :public, :repository, :repository_private) } + + it "does not include statistics if user is not a member" do + get api("/projects/#{project.id}", user), params: { statistics: true } + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'statistics' + end + + it "includes statistics if user is a member" do + project.add_developer(user) + + get api("/projects/#{project.id}", user), params: { statistics: true } + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'statistics' + end + end + it "includes import_error if user can admin project" do get api("/projects/#{project.id}", user) -- GitLab From 19cb1985c76ae756194a05f21af0d02caedb60c2 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Thu, 7 Feb 2019 12:04:09 +1300 Subject: [PATCH 11/78] Removing sensitive properties from ProjectType defaultBranch and ciConfigPath should only be available to users with the :download_code permission for the Project, as the respository might be private. When implementing the authorize check on these properties, it was found that our current Graphql::Authorize::Instrumentation class does not work with fields that resolve to subclasses of GraphQL::Schema::Scalar, like GraphQL::STRING_TYPE. After discussion with other Create Team members, it has been decided that because the GraphQL API is not GA, to remove these properties from ProjectType, and instead implement them as part of epic https://gitlab.com/groups/gitlab-org/-/epics/711 Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/55316 --- app/graphql/types/project_type.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index d25c8c8bd90..8a6ccb8c5c1 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -16,7 +16,6 @@ module Types field :description, GraphQL::STRING_TYPE, null: true - field :default_branch, GraphQL::STRING_TYPE, null: true field :tag_list, GraphQL::STRING_TYPE, null: true field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true @@ -59,7 +58,6 @@ module Types end field :import_status, GraphQL::STRING_TYPE, null: true - field :ci_config_path, GraphQL::STRING_TYPE, null: true field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true -- GitLab From 9d046c8704c0e7df18d2f9e380e987d22b9a0b2e Mon Sep 17 00:00:00 2001 From: Mark Chao Date: Mon, 18 Feb 2019 11:51:56 +0800 Subject: [PATCH 12/78] Fix git clone revealing private repo's presence Ensure redirection to path with .git suffix regardless whether project exists or not. --- changelogs/unreleased/security-50334.yml | 5 + config/routes/git_http.rb | 2 +- lib/constraints/project_url_constrainer.rb | 3 +- .../project_url_constrainer_spec.rb | 4 + spec/requests/git_http_spec.rb | 134 +++++++++--------- 5 files changed, 82 insertions(+), 66 deletions(-) create mode 100644 changelogs/unreleased/security-50334.yml diff --git a/changelogs/unreleased/security-50334.yml b/changelogs/unreleased/security-50334.yml new file mode 100644 index 00000000000..828ef82b517 --- /dev/null +++ b/changelogs/unreleased/security-50334.yml @@ -0,0 +1,5 @@ +--- +title: Fix git clone revealing private repo's presence +merge_request: +author: +type: security diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb index ec5c68f81df..a959d40881b 100644 --- a/config/routes/git_http.rb +++ b/config/routes/git_http.rb @@ -40,7 +40,7 @@ scope(path: '*namespace_id/:project_id', # /info/refs?service=git-receive-pack, but nothing else. # git_http_handshake = lambda do |request| - ::Constraints::ProjectUrlConstrainer.new.matches?(request) && + ::Constraints::ProjectUrlConstrainer.new.matches?(request, existence_check: false) && (request.query_string.blank? || request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)) end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index eadfbf7bc01..d41490d2ebd 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -2,12 +2,13 @@ module Constraints class ProjectUrlConstrainer - def matches?(request) + def matches?(request, existence_check: true) namespace_path = request.params[:namespace_id] project_path = request.params[:project_id] || request.params[:id] full_path = [namespace_path, project_path].join('/') return false unless ProjectPathValidator.valid_path?(full_path) + return true unless existence_check # We intentionally allow SELECT(*) here so result of this query can be used # as cache for further Project.find_by_full_path calls within request diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index c96e7ab8495..3496b01ebcc 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -16,6 +16,10 @@ describe Constraints::ProjectUrlConstrainer do let(:request) { build_request('foo', 'bar') } it { expect(subject.matches?(request)).to be_falsey } + + context 'existence_check is false' do + it { expect(subject.matches?(request, existence_check: false)).to be_truthy } + end end context "project id ending with .git" do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 5b625fd47be..bfa178f5cae 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -104,6 +104,70 @@ describe 'Git HTTP requests' do end end + shared_examples_for 'project path without .git suffix' do + context "GET info/refs" do + let(:path) { "/#{project_path}/info/refs" } + + context "when no params are added" do + before do + get path + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs") + end + end + + context "when the upload-pack service is requested" do + let(:params) { { service: 'git-upload-pack' } } + + before do + get path, params: params + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") + end + end + + context "when the receive-pack service is requested" do + let(:params) { { service: 'git-receive-pack' } } + + before do + get path, params: params + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") + end + end + + context "when the params are anything else" do + let(:params) { { service: 'git-implode-pack' } } + + before do + get path, params: params + end + + it "redirects to the sign-in page" do + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "POST git-upload-pack" do + it "fails to find a route" do + expect { clone_post(project_path) }.to raise_error(ActionController::RoutingError) + end + end + + context "POST git-receive-pack" do + it "fails to find a route" do + expect { push_post(project_path) }.to raise_error(ActionController::RoutingError) + end + end + end + describe "User with no identities" do let(:user) { create(:user) } @@ -143,6 +207,10 @@ describe 'Git HTTP requests' do expect(response).to have_gitlab_http_status(:unprocessable_entity) end end + + it_behaves_like 'project path without .git suffix' do + let(:project_path) { "#{user.namespace.path}/project.git-project" } + end end end @@ -706,70 +774,8 @@ describe 'Git HTTP requests' do end end - context "when the project path doesn't end in .git" do - let(:project) { create(:project, :repository, :public, path: 'project.git-project') } - - context "GET info/refs" do - let(:path) { "/#{project.full_path}/info/refs" } - - context "when no params are added" do - before do - get path - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs") - end - end - - context "when the upload-pack service is requested" do - let(:params) { { service: 'git-upload-pack' } } - - before do - get path, params: params - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}") - end - end - - context "when the receive-pack service is requested" do - let(:params) { { service: 'git-receive-pack' } } - - before do - get path, params: params - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}") - end - end - - context "when the params are anything else" do - let(:params) { { service: 'git-implode-pack' } } - - before do - get path, params: params - end - - it "redirects to the sign-in page" do - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context "POST git-upload-pack" do - it "fails to find a route" do - expect { clone_post(project.full_path) }.to raise_error(ActionController::RoutingError) - end - end - - context "POST git-receive-pack" do - it "fails to find a route" do - expect { push_post(project.full_path) }.to raise_error(ActionController::RoutingError) - end - end + it_behaves_like 'project path without .git suffix' do + let(:project_path) { create(:project, :repository, :public, path: 'project.git-project').full_path } end context "retrieving an info/refs file" do -- GitLab From fc8c1a77d36003795586fe076243b6eb90db6f03 Mon Sep 17 00:00:00 2001 From: Tiger Date: Wed, 13 Feb 2019 11:11:28 +1100 Subject: [PATCH 13/78] Validate session key when authorizing with GCP to create a cluster It was previously possible to link a GCP account to another user's GitLab account by having them visit the callback URL, as there was no check that they were the initiator of the request. We now reject the callback unless the state parameter matches the one added to the initiating user's session. --- .../google_api/authorizations_controller.rb | 32 ++++++---- .../security-kubernetes-google-login-csrf.yml | 5 ++ .../authorizations_controller_spec.rb | 60 +++++++++++++------ 3 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 changelogs/unreleased/security-kubernetes-google-login-csrf.yml diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb index dd9f5af61b3..ed0995e7ffd 100644 --- a/app/controllers/google_api/authorizations_controller.rb +++ b/app/controllers/google_api/authorizations_controller.rb @@ -2,6 +2,10 @@ module GoogleApi class AuthorizationsController < ApplicationController + include Gitlab::Utils::StrongMemoize + + before_action :validate_session_key! + def callback token, expires_at = GoogleApi::CloudPlatform::Client .new(nil, callback_google_api_auth_url) @@ -11,21 +15,27 @@ module GoogleApi session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = expires_at.to_s - state_redirect_uri = redirect_uri_from_session_key(params[:state]) - - if state_redirect_uri - redirect_to state_redirect_uri - else - redirect_to root_path - end + redirect_to redirect_uri_from_session end private - def redirect_uri_from_session_key(state) - key = GoogleApi::CloudPlatform::Client - .session_key_for_redirect_uri(params[:state]) - session[key] if key + def validate_session_key! + access_denied! unless redirect_uri_from_session.present? + end + + def redirect_uri_from_session + strong_memoize(:redirect_uri_from_session) do + if params[:state].present? + session[session_key_for_redirect_uri(params[:state])] + else + nil + end + end + end + + def session_key_for_redirect_uri(state) + GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(state) end end end diff --git a/changelogs/unreleased/security-kubernetes-google-login-csrf.yml b/changelogs/unreleased/security-kubernetes-google-login-csrf.yml new file mode 100644 index 00000000000..2f87100a8dd --- /dev/null +++ b/changelogs/unreleased/security-kubernetes-google-login-csrf.yml @@ -0,0 +1,5 @@ +--- +title: Validate session key when authorizing with GCP to create a cluster +merge_request: +author: +type: security diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb index 1e8e82da4f3..d9ba85cf56a 100644 --- a/spec/controllers/google_api/authorizations_controller_spec.rb +++ b/spec/controllers/google_api/authorizations_controller_spec.rb @@ -6,7 +6,7 @@ describe GoogleApi::AuthorizationsController do let(:token) { 'token' } let(:expires_at) { 1.hour.since.strftime('%s') } - subject { get :callback, params: { code: 'xxx', state: @state } } + subject { get :callback, params: { code: 'xxx', state: state } } before do sign_in(user) @@ -15,35 +15,57 @@ describe GoogleApi::AuthorizationsController do .to receive(:get_token).and_return([token, expires_at]) end - it 'sets token and expires_at in session' do - subject + shared_examples_for 'access denied' do + it 'returns a 404' do + subject - expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]) - .to eq(token) - expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]) - .to eq(expires_at) + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil + expect(response).to have_http_status(:not_found) + end end - context 'when redirect uri key is stored in state' do - set(:project) { create(:project) } - let(:redirect_uri) { project_clusters_url(project).to_s } + context 'session key is present' do + let(:session_key) { 'session-key' } + let(:redirect_uri) { 'example.com' } before do - @state = GoogleApi::CloudPlatform::Client - .new_session_key_for_redirect_uri do |key| - session[key] = redirect_uri + session[GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(session_key)] = redirect_uri + end + + context 'session key matches state param' do + let(:state) { session_key } + + it 'sets token and expires_at in session' do + subject + + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]) + .to eq(token) + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]) + .to eq(expires_at) + end + + it 'redirects to the URL stored in state param' do + expect(subject).to redirect_to(redirect_uri) end end - it 'redirects to the URL stored in state param' do - expect(subject).to redirect_to(redirect_uri) + context 'session key does not match state param' do + let(:state) { 'bad-key' } + + it_behaves_like 'access denied' end - end - context 'when redirection url is not stored in state' do - it 'redirects to root_path' do - expect(subject).to redirect_to(root_path) + context 'state param is blank' do + let(:state) { '' } + + it_behaves_like 'access denied' end end + + context 'state param is present, but session key is blank' do + let(:state) { 'session-key' } + + it_behaves_like 'access denied' + end end end -- GitLab From 54a5d513e5f068c53fad3b2dac04998f5e9afd88 Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Tue, 19 Feb 2019 13:27:53 -0700 Subject: [PATCH 14/78] Fixing CI icon mismatch MR list, and related MRs and branches were using a deprecated helper. Created a new icon haml file to help move these forward. --- app/views/ci/status/_icon.html.haml | 12 +++ .../projects/issues/_merge_requests.html.haml | 2 +- .../issues/_related_branches.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- ...-merge-requests-page-and-the-mr-itself.yml | 5 ++ spec/views/ci/status/_icon.html.haml_spec.rb | 88 +++++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 app/views/ci/status/_icon.html.haml create mode 100644 changelogs/unreleased/45305-ci-status-icon-mismatch-on-merge-requests-page-and-the-mr-itself.yml create mode 100644 spec/views/ci/status/_icon.html.haml_spec.rb diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml new file mode 100644 index 00000000000..bfdf9c2ec9f --- /dev/null +++ b/app/views/ci/status/_icon.html.haml @@ -0,0 +1,12 @@ +- status = local_assigns.fetch(:status) +- size = local_assigns.fetch(:size, 16) +- link = local_assigns.fetch(:link, true) +- title = local_assigns.fetch(:title, nil) +- css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} #{'has-tooltip' if title.present?}" + +- if link && status.has_details? + = link_to status.details_path, class: css_classes, title: title, data: { html: title.present? } do + = sprite_icon(status.icon, size: size) +- else + %span{ class: css_classes, title: title, data: { html: title.present? } } + = sprite_icon(status.icon, size: size) \ No newline at end of file diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 310e339ac8d..ab3d7907ad8 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -27,7 +27,7 @@ = merge_request.to_reference %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2 - if merge_request.can_read_pipeline? - = render_pipeline_status(merge_request.head_pipeline, tooltip_placement: 'bottom') + = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), link: true - elsif has_any_head_pipeline = icon('blank fw') diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index ffdd96870ef..f8d1d64100a 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -8,7 +8,7 @@ - pipeline = @project.pipeline_for(branch, target.sha) if target - if can?(current_user, :read_pipeline, pipeline) %span.related-branch-ci-status - = render_pipeline_status(pipeline) + = render 'ci/status/icon', status: pipeline.detailed_status(current_user), link: true %span.related-branch-info %strong = link_to branch, project_compare_path(@project, from: @project.default_branch, to: branch), class: "ref-name" diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index ac29cd8f679..4111e823701 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -48,7 +48,7 @@ CLOSED - if can?(current_user, :read_pipeline, merge_request.head_pipeline) %li.issuable-pipeline-status.d-none.d-sm-inline-block - = render_pipeline_status(merge_request.head_pipeline) + = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), link: true - if merge_request.open? && merge_request.broken? %li.issuable-pipeline-broken.d-none.d-sm-inline-block = link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do diff --git a/changelogs/unreleased/45305-ci-status-icon-mismatch-on-merge-requests-page-and-the-mr-itself.yml b/changelogs/unreleased/45305-ci-status-icon-mismatch-on-merge-requests-page-and-the-mr-itself.yml new file mode 100644 index 00000000000..64ab76a2b05 --- /dev/null +++ b/changelogs/unreleased/45305-ci-status-icon-mismatch-on-merge-requests-page-and-the-mr-itself.yml @@ -0,0 +1,5 @@ +--- +title: Fix pipeline status icon mismatch +merge_request: 25407 +author: +type: fixed diff --git a/spec/views/ci/status/_icon.html.haml_spec.rb b/spec/views/ci/status/_icon.html.haml_spec.rb new file mode 100644 index 00000000000..43806446164 --- /dev/null +++ b/spec/views/ci/status/_icon.html.haml_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe 'ci/status/_icon' do + let(:user) { create(:user) } + let(:project) { create(:project, :private) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when rendering status for build' do + let(:build) do + create(:ci_build, :success, pipeline: pipeline) + end + + context 'when user has ability to see details' do + before do + project.add_developer(user) + end + + it 'has link to build details page' do + details_path = project_job_path(project, build) + + render_status(build) + + expect(rendered).to have_link(href: details_path) + end + end + + context 'when user do not have ability to see build details' do + before do + render_status(build) + end + + it 'contains build status text' do + expect(rendered).to have_css('.ci-status-icon.ci-status-icon-success') + end + + it 'does not contain links' do + expect(rendered).not_to have_link + end + end + end + + context 'when rendering status for external job' do + context 'when user has ability to see commit status details' do + before do + project.add_developer(user) + end + + context 'status has external target url' do + before do + external_job = create(:generic_commit_status, + status: :running, + pipeline: pipeline, + target_url: 'http://gitlab.com') + + render_status(external_job) + end + + it 'contains valid commit status text' do + expect(rendered).to have_css('.ci-status-icon.ci-status-icon-running') + end + + it 'has link to external status page' do + expect(rendered).to have_link(href: 'http://gitlab.com') + end + end + + context 'status do not have external target url' do + before do + external_job = create(:generic_commit_status, status: :canceled) + + render_status(external_job) + end + + it 'contains valid commit status text' do + expect(rendered).to have_css('.ci-status-icon.ci-status-icon-canceled') + end + + it 'has link to external status page' do + expect(rendered).not_to have_link + end + end + end + end + + def render_status(resource) + render 'ci/status/icon', status: resource.detailed_status(user) + end +end -- GitLab From 38f3c2e18684f94f75ab52e5039203e8321ed7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Wed, 13 Feb 2019 11:50:24 +0100 Subject: [PATCH 15/78] Remove link after issue move when no permissions Don't show new issue link after move when a user does not have permissions to display the new issue --- app/mailers/emails/issues.rb | 1 + app/views/notify/issue_moved_email.html.haml | 11 ++-- app/views/notify/issue_moved_email.text.erb | 4 ++ .../unreleased/security-2799-emails.yml | 5 ++ spec/mailers/notify_spec.rb | 56 ++++++++++++++----- 5 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/security-2799-emails.yml diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 654ae211310..d2e334fb856 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -74,6 +74,7 @@ module Emails @new_issue = new_issue @new_project = new_issue.project + @can_access_project = recipient.can?(:read_project, @new_project) mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason)) end diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml index 472c31e9a5e..b766cb1a523 100644 --- a/app/views/notify/issue_moved_email.html.haml +++ b/app/views/notify/issue_moved_email.html.haml @@ -1,6 +1,9 @@ %p Issue was moved to another project. -%p - New issue: - = link_to project_issue_url(@new_project, @new_issue) do - = @new_issue.title +- if @can_access_project + %p + New issue: + = link_to project_issue_url(@new_project, @new_issue) do + = @new_issue.title +- else + You don't have access to the project. diff --git a/app/views/notify/issue_moved_email.text.erb b/app/views/notify/issue_moved_email.text.erb index 66ede43635b..985e689aa9d 100644 --- a/app/views/notify/issue_moved_email.text.erb +++ b/app/views/notify/issue_moved_email.text.erb @@ -1,4 +1,8 @@ Issue was moved to another project. +<% if @can_access_project %> New issue location: <%= project_issue_url(@new_project, @new_issue) %> +<% else %> +You don't have access to the project. +<% end %> diff --git a/changelogs/unreleased/security-2799-emails.yml b/changelogs/unreleased/security-2799-emails.yml new file mode 100644 index 00000000000..dbf1207810e --- /dev/null +++ b/changelogs/unreleased/security-2799-emails.yml @@ -0,0 +1,5 @@ +--- +title: Don't show new issue link after move when a user does not have permissions +merge_request: +author: +type: security diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 4f578c48d5b..418f707a130 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -194,23 +194,53 @@ describe Notify do let(:new_issue) { create(:issue) } subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) } - it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do - let(:model) { issue } - end - it_behaves_like 'it should show Gmail Actions View Issue link' - it_behaves_like 'an unsubscribeable thread' + context 'when a user has permissions to access the new issue' do + before do + new_issue.project.add_developer(recipient) + end + + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end + it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' + + it 'contains description about action taken' do + is_expected.to have_body_text 'Issue was moved to another project' + end + + it 'has the correct subject and body' do + new_issue_url = project_issue_path(new_issue.project, new_issue) - it 'contains description about action taken' do - is_expected.to have_body_text 'Issue was moved to another project' + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(new_issue_url) + is_expected.to have_body_text(project_issue_path(project, issue)) + end + end + + it 'contains the issue title' do + is_expected.to have_body_text new_issue.title + end end - it 'has the correct subject and body' do - new_issue_url = project_issue_path(new_issue.project, new_issue) + context 'when a user does not permissions to access the new issue' do + it 'has the correct subject and body' do + new_issue_url = project_issue_path(new_issue.project, new_issue) - aggregate_failures do - is_expected.to have_referable_subject(issue, reply: true) - is_expected.to have_body_text(new_issue_url) - is_expected.to have_body_text(project_issue_path(project, issue)) + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.not_to have_body_text(new_issue_url) + is_expected.to have_body_text(project_issue_path(project, issue)) + end + end + + it 'does not contain the issue title' do + is_expected.not_to have_body_text new_issue.title + end + + it 'contains information about missing permissions' do + is_expected.to have_body_text "You don't have access to the project." end end end -- GitLab From 211c4e5985bf40afe7cf2391c76a6cfde153fb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C5=82gorzata=20Ksionek?= Date: Tue, 12 Feb 2019 13:29:47 +0100 Subject: [PATCH 16/78] Change policy regarding group visibility --- .../projects/group_links_controller.rb | 5 ++- .../projects/group_links/create_service.rb | 10 ++++- ...groups-as-members-to-your-project-idor.yml | 6 +++ lib/api/projects.rb | 15 +++----- .../groups/shared_projects_controller_spec.rb | 2 + .../projects/group_links_controller_spec.rb | 37 +++++++++++++++++++ .../projects/members/invite_group_spec.rb | 2 + .../settings/user_manages_group_links_spec.rb | 1 + spec/requests/api/projects_spec.rb | 12 ++++++ .../group_links/create_service_spec.rb | 8 ++++ 10 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/2802-security-add-public-internal-groups-as-members-to-your-project-idor.yml diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 7c713c19762..bc942ba9288 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -13,9 +13,10 @@ class Projects::GroupLinksController < Projects::ApplicationController group = Group.find(params[:link_group_id]) if params[:link_group_id].present? if group - return render_404 unless can?(current_user, :read_group, group) + result = Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group) + return render_404 if result[:http_status] == 404 - Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group) + flash[:alert] = result[:message] if result[:http_status] == 409 else flash[:alert] = 'Please select a group.' end diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb index 1392775f805..e3d5bea0852 100644 --- a/app/services/projects/group_links/create_service.rb +++ b/app/services/projects/group_links/create_service.rb @@ -4,13 +4,19 @@ module Projects module GroupLinks class CreateService < BaseService def execute(group) - return false unless group + return error('Not Found', 404) unless group && can?(current_user, :read_namespace, group) - project.project_group_links.create( + link = project.project_group_links.new( group: group, group_access: params[:link_group_access], expires_at: params[:expires_at] ) + + if link.save + success(link: link) + else + error(link.errors.full_messages.to_sentence, 409) + end end end end diff --git a/changelogs/unreleased/2802-security-add-public-internal-groups-as-members-to-your-project-idor.yml b/changelogs/unreleased/2802-security-add-public-internal-groups-as-members-to-your-project-idor.yml new file mode 100644 index 00000000000..27ad151cd06 --- /dev/null +++ b/changelogs/unreleased/2802-security-add-public-internal-groups-as-members-to-your-project-idor.yml @@ -0,0 +1,6 @@ +--- +title: Remove the possibility to share a project with a group that a user is not a member + of +merge_request: +author: +type: security diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6a93ef9f3ad..a7b4dc06832 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -436,27 +436,24 @@ module API end params do requires :group_id, type: Integer, desc: 'The ID of a group' - requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level' + requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level' optional :expires_at, type: Date, desc: 'Share expiration date' end post ":id/share" do authorize! :admin_project, user_project group = Group.find_by_id(params[:group_id]) - unless group && can?(current_user, :read_group, group) - not_found!('Group') - end - unless user_project.allowed_to_share_with_group? break render_api_error!("The project sharing with group is disabled", 400) end - link = user_project.project_group_links.new(declared_params(include_missing: false)) + result = ::Projects::GroupLinks::CreateService.new(user_project, current_user, declared_params(include_missing: false)) + .execute(group) - if link.save - present link, with: Entities::ProjectGroupLink + if result[:status] == :success + present result[:link], with: Entities::ProjectGroupLink else - render_api_error!(link.errors.full_messages.first, 409) + render_api_error!(result[:message], result[:http_status]) end end diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb index dab7700cf64..b0c20fb5a90 100644 --- a/spec/controllers/groups/shared_projects_controller_spec.rb +++ b/spec/controllers/groups/shared_projects_controller_spec.rb @@ -6,6 +6,8 @@ describe Groups::SharedProjectsController do end def share_project(project) + group.add_developer(user) + Projects::GroupLinks::CreateService.new( project, user, diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 675eeff8d12..ce021b2f085 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -65,8 +65,24 @@ describe Projects::GroupLinksController do end end + context 'when user does not have access to the public group' do + let(:group) { create(:group, :public) } + + include_context 'link project to group' + + it 'renders 404' do + expect(response.status).to eq 404 + end + + it 'does not share project with that group' do + expect(group.shared_projects).not_to include project + end + end + context 'when project group id equal link group id' do before do + group2.add_developer(user) + post(:create, params: { namespace_id: project.namespace, project_id: project, @@ -102,5 +118,26 @@ describe Projects::GroupLinksController do expect(flash[:alert]).to eq('Please select a group.') end end + + context 'when link is not persisted in the database' do + before do + allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute) + .and_return({ status: :error, http_status: 409, message: 'error' }) + + post(:create, params: { + namespace_id: project.namespace, + project_id: project, + link_group_id: group.id, + link_group_access: ProjectGroupLink.default_access + }) + end + + it 'redirects to project group links page' do + expect(response).to redirect_to( + project_project_members_path(project) + ) + expect(flash[:alert]).to eq('error') + end + end end end diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb index fceead0b45e..b2d2dba55f1 100644 --- a/spec/features/projects/members/invite_group_spec.rb +++ b/spec/features/projects/members/invite_group_spec.rb @@ -27,6 +27,7 @@ describe 'Project > Members > Invite group', :js do before do project.add_maintainer(maintainer) + group_to_share_with.add_guest(maintainer) sign_in(maintainer) end @@ -112,6 +113,7 @@ describe 'Project > Members > Invite group', :js do before do project.add_maintainer(maintainer) + group.add_guest(maintainer) sign_in(maintainer) visit project_settings_members_path(project) diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb index 676659b90c3..e5a58c44e41 100644 --- a/spec/features/projects/settings/user_manages_group_links_spec.rb +++ b/spec/features/projects/settings/user_manages_group_links_spec.rb @@ -10,6 +10,7 @@ describe 'Projects > Settings > User manages group links' do before do project.add_maintainer(user) + group_market.add_guest(user) sign_in(user) share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index cfa7a1a31a3..feff53ebc25 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1484,6 +1484,9 @@ describe API::Projects do describe "POST /projects/:id/share" do let(:group) { create(:group) } + before do + group.add_developer(user) + end it "shares project with group" do expires_at = 10.days.from_now.to_date @@ -1534,6 +1537,15 @@ describe API::Projects do expect(response).to have_gitlab_http_status(400) expect(json_response['error']).to eq 'group_access does not have a valid value' end + + it "returns a 409 error when link is not saved" do + allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute) + .and_return({ status: :error, http_status: 409, message: 'error' }) + + post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } + + expect(response).to have_gitlab_http_status(409) + end end describe 'DELETE /projects/:id/share/:group_id' do diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb index ffb270d277e..68fd82b4cbe 100644 --- a/spec/services/projects/group_links/create_service_spec.rb +++ b/spec/services/projects/group_links/create_service_spec.rb @@ -12,6 +12,10 @@ describe Projects::GroupLinks::CreateService, '#execute' do end let(:subject) { described_class.new(project, user, opts) } + before do + group.add_developer(user) + end + it 'adds group to project' do expect { subject.execute(group) }.to change { project.project_group_links.count }.from(0).to(1) end @@ -19,4 +23,8 @@ describe Projects::GroupLinks::CreateService, '#execute' do it 'returns false if group is blank' do expect { subject.execute(nil) }.not_to change { project.project_group_links.count } end + + it 'returns error if user is not allowed to share with a group' do + expect { subject.execute(create :group) }.not_to change { project.project_group_links.count } + end end -- GitLab From 52c910eeca47f140246d003fbb6b4748d1be8bb8 Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Wed, 20 Feb 2019 12:20:09 -0700 Subject: [PATCH 17/78] Remove deprecated ci status helper function After changing all places that used the function, we can now remove it. --- app/helpers/ci_status_helper.rb | 6 ------ app/views/ci/status/_icon.html.haml | 2 +- spec/views/ci/status/_icon.html.haml_spec.rb | 1 + 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 923a06a0512..dfeeecf1228 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -123,12 +123,6 @@ module CiStatusHelper icon_size: 24) end - def render_pipeline_status(pipeline, tooltip_placement: 'left') - project = pipeline.project - path = project_pipeline_path(project, pipeline) - render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement) - end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index bfdf9c2ec9f..29ce9cd1afd 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -9,4 +9,4 @@ = sprite_icon(status.icon, size: size) - else %span{ class: css_classes, title: title, data: { html: title.present? } } - = sprite_icon(status.icon, size: size) \ No newline at end of file + = sprite_icon(status.icon, size: size) diff --git a/spec/views/ci/status/_icon.html.haml_spec.rb b/spec/views/ci/status/_icon.html.haml_spec.rb index 43806446164..626159fc512 100644 --- a/spec/views/ci/status/_icon.html.haml_spec.rb +++ b/spec/views/ci/status/_icon.html.haml_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'spec_helper' describe 'ci/status/_icon' do -- GitLab From b11d018bd59a3c94086dbeb11f38db81e441a1e2 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Thu, 7 Feb 2019 12:46:41 +1300 Subject: [PATCH 18/78] Add changelog entry --- .../unreleased/security-protect-private-repo-information.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/security-protect-private-repo-information.yml diff --git a/changelogs/unreleased/security-protect-private-repo-information.yml b/changelogs/unreleased/security-protect-private-repo-information.yml new file mode 100644 index 00000000000..8b1a528206d --- /dev/null +++ b/changelogs/unreleased/security-protect-private-repo-information.yml @@ -0,0 +1,5 @@ +--- +title: Fix leaking private repository information in API +merge_request: +author: +type: security -- GitLab From d72b1cd0b5b01d6fec6b93d9dfe84f8302083072 Mon Sep 17 00:00:00 2001 From: Mark Chao Date: Wed, 13 Feb 2019 16:24:26 +0800 Subject: [PATCH 19/78] Check snippet attached file to be moved is within designated directory Previously one could move any temp/ sub folder around. --- app/uploaders/file_mover.rb | 8 ++++++ changelogs/unreleased/security-56348.yml | 5 ++++ spec/controllers/snippets_controller_spec.rb | 4 +++ spec/support/helpers/file_mover_helpers.rb | 12 ++++++++ spec/uploaders/file_mover_spec.rb | 30 ++++++++++++++++++++ 5 files changed, 59 insertions(+) create mode 100644 changelogs/unreleased/security-56348.yml create mode 100644 spec/support/helpers/file_mover_helpers.rb diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb index a7f8615e9ba..236b7ed2b3d 100644 --- a/app/uploaders/file_mover.rb +++ b/app/uploaders/file_mover.rb @@ -11,6 +11,8 @@ class FileMover end def execute + return unless valid? + move if update_markdown @@ -21,6 +23,12 @@ class FileMover private + def valid? + Pathname.new(temp_file_path).realpath.to_path.start_with?( + (Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path + ) + end + def move FileUtils.mkdir_p(File.dirname(file_path)) FileUtils.move(temp_file_path, file_path) diff --git a/changelogs/unreleased/security-56348.yml b/changelogs/unreleased/security-56348.yml new file mode 100644 index 00000000000..a289e4e9077 --- /dev/null +++ b/changelogs/unreleased/security-56348.yml @@ -0,0 +1,5 @@ +--- +title: Check snippet attached file to be moved is within designated directory +merge_request: +author: +type: security diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 5c6858dc7b2..77a94f26d8c 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -205,6 +205,8 @@ describe SnippetsController do end context 'when the snippet description contains a file' do + include FileMoverHelpers + let(:picture_file) { '/-/system/temp/secret56/picture.jpg' } let(:text_file) { '/-/system/temp/secret78/text.txt' } let(:description) do @@ -215,6 +217,8 @@ describe SnippetsController do before do allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:move) + stub_file_mover(text_file) + stub_file_mover(picture_file) end subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) } diff --git a/spec/support/helpers/file_mover_helpers.rb b/spec/support/helpers/file_mover_helpers.rb new file mode 100644 index 00000000000..1ba7cc03354 --- /dev/null +++ b/spec/support/helpers/file_mover_helpers.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module FileMoverHelpers + def stub_file_mover(file_path, stub_real_path: nil) + file_name = File.basename(file_path) + allow(Pathname).to receive(:new).and_call_original + + expect_next_instance_of(Pathname, a_string_including(file_name)) do |pathname| + allow(pathname).to receive(:realpath) { stub_real_path || pathname.cleanpath } + end + end +end diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index a28d7445b1c..e474a714b10 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe FileMover do + include FileMoverHelpers + let(:filename) { 'banana_sample.gif' } let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) } @@ -19,6 +21,8 @@ describe FileMover do expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path)) allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true) allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10) + + stub_file_mover(temp_file_path) end context 'when move and field update successful' do @@ -65,4 +69,30 @@ describe FileMover do end end end + + context 'security' do + context 'when relative path is involved' do + let(:temp_file_path) { File.join('uploads/-/system/temp', '..', 'another_subdir_of_temp') } + + it 'does not trigger move if path is outside designated directory' do + stub_file_mover('uploads/-/system/another_subdir_of_temp') + expect(FileUtils).not_to receive(:move) + + subject + + expect(snippet.reload.description).to eq(temp_description) + end + end + + context 'when symlink is involved' do + it 'does not trigger move if path is outside designated directory' do + stub_file_mover(temp_file_path, stub_real_path: Pathname('/etc')) + expect(FileUtils).not_to receive(:move) + + subject + + expect(snippet.reload.description).to eq(temp_description) + end + end + end end -- GitLab From af16fd687e2e5b15a63e6e51d76847512ae8ee72 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Wed, 13 Feb 2019 09:46:59 +1300 Subject: [PATCH 20/78] Do not allow local urls in Kubernetes form Use existing `public_url` validation to block various local urls. Note that this validation will allow local urls if the "Allow requests to the local network from hooks and services" admin setting is enabled. Block KubeClient from using local addresses It will also respect `allow_local_requests_from_hooks_and_services` so if that is enabled KubeClinet will allow local addresses --- app/models/clusters/platforms/kubernetes.rb | 2 +- .../security-kubernetes-local-ssrf.yml | 5 ++++ lib/gitlab/kubernetes/kube_client.rb | 8 +++++ .../lib/gitlab/kubernetes/kube_client_spec.rb | 30 +++++++++++++++++++ .../clusters/platforms/kubernetes_spec.rb | 16 ++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/security-kubernetes-local-ssrf.yml diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 46d0898014e..814fc591408 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -41,7 +41,7 @@ module Clusters validate :no_namespace, unless: :allow_user_defined_namespace? # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) - validates :api_url, url: true, presence: true + validates :api_url, public_url: true, presence: true validates :token, presence: true validates :ca_cert, certificate: true, allow_blank: true, if: :ca_cert_changed? diff --git a/changelogs/unreleased/security-kubernetes-local-ssrf.yml b/changelogs/unreleased/security-kubernetes-local-ssrf.yml new file mode 100644 index 00000000000..7a2ad092339 --- /dev/null +++ b/changelogs/unreleased/security-kubernetes-local-ssrf.yml @@ -0,0 +1,5 @@ +--- +title: Block local URLs for Kubernetes integration +merge_request: +author: +type: security diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index 624c2c67551..de14df56555 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -82,6 +82,8 @@ module Gitlab def initialize(api_prefix, **kubeclient_options) @api_prefix = api_prefix @kubeclient_options = kubeclient_options.merge(http_max_redirects: 0) + + validate_url! end def create_or_update_cluster_role_binding(resource) @@ -118,6 +120,12 @@ module Gitlab private + def validate_url! + return if Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services? + + Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false) + end + def cluster_role_binding_exists?(resource) get_cluster_role_binding(resource.metadata.name) rescue ::Kubeclient::ResourceNotFoundError diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 02364e92149..978e64c4407 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -50,6 +50,36 @@ describe Gitlab::Kubernetes::KubeClient do end end + describe '#initialize' do + shared_examples 'local address' do + it 'blocks local addresses' do + expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + end + + context 'when local requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_hooks_and_services: true) + end + + it 'allows local addresses' do + expect { client }.not_to raise_error + end + end + end + + context 'localhost address' do + let(:api_url) { 'http://localhost:22' } + + it_behaves_like 'local address' + end + + context 'private network address' do + let(:api_url) { 'http://192.168.1.2:3003' } + + it_behaves_like 'local address' + end + end + describe '#core_client' do subject { client.core_client } diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 4068d98d8f7..3b32ca8df05 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -98,6 +98,22 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching it { expect(kubernetes.save).to be_truthy } end + + context 'when api_url is localhost' do + let(:api_url) { 'http://localhost:22' } + + it { expect(kubernetes.save).to be_falsey } + + context 'Application settings allows local requests' do + before do + allow(ApplicationSetting) + .to receive(:current) + .and_return(ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: true)) + end + + it { expect(kubernetes.save).to be_truthy } + end + end end context 'when validates token' do -- GitLab From 445709345f8a451b7882c81166063b8948688e0f Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Fri, 15 Feb 2019 17:11:16 -0200 Subject: [PATCH 21/78] Stop linking to unrecognized package sources --- .../viewers/_dependency_manager.html.haml | 5 --- lib/gitlab/dependency_linker/base_linker.rb | 14 +++++++ .../dependency_linker/composer_json_linker.rb | 4 +- .../dependency_linker/gemfile_linker.rb | 30 ++++++++++--- .../dependency_linker/gemspec_linker.rb | 2 +- lib/gitlab/dependency_linker/method_linker.rb | 10 +++-- lib/gitlab/dependency_linker/package.rb | 19 +++++++++ .../dependency_linker/package_json_linker.rb | 21 ++++------ .../dependency_linker/parser/gemfile.rb | 40 ++++++++++++++++++ .../dependency_linker/podfile_linker.rb | 11 ++++- .../dependency_linker/podspec_linker.rb | 2 +- .../features/projects/blobs/blob_show_spec.rb | 5 +-- .../composer_json_linker_spec.rb | 4 +- .../dependency_linker/gemfile_linker_spec.rb | 9 ++-- .../dependency_linker/gemspec_linker_spec.rb | 4 +- .../package_json_linker_spec.rb | 18 +++++--- .../dependency_linker/parser/gemfile_spec.rb | 42 +++++++++++++++++++ .../dependency_linker/podfile_linker_spec.rb | 5 ++- .../dependency_linker/podspec_linker_spec.rb | 4 +- 19 files changed, 198 insertions(+), 51 deletions(-) create mode 100644 lib/gitlab/dependency_linker/package.rb create mode 100644 lib/gitlab/dependency_linker/parser/gemfile.rb create mode 100644 spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb diff --git a/app/views/projects/blob/viewers/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml index 87aa7c1dbf8..5970d41fdab 100644 --- a/app/views/projects/blob/viewers/_dependency_manager.html.haml +++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml @@ -3,9 +3,4 @@ This project manages its dependencies using %strong= viewer.manager_name - - if viewer.package_name - and defines a #{viewer.package_type} named - %strong< - = link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer' - = link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer' diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index ac2efe598b4..6b44d5b1518 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -4,6 +4,7 @@ module Gitlab module DependencyLinker class BaseLinker URL_REGEX = %r{https?://[^'" ]+}.freeze + GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze class_attribute :file_type @@ -29,6 +30,19 @@ module Gitlab highlighted_lines.join.html_safe end + def external_url(name, external_ref) + return if external_ref =~ GIT_INVALID_URL_REGEX + + case external_ref + when /\A#{URL_REGEX}\z/ + external_ref + when /\A#{REPO_REGEX}\z/ + github_url(external_ref) + else + package_url(name) + end + end + private def link_dependencies diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb index 22d2bead891..4b8862b31ee 100644 --- a/lib/gitlab/dependency_linker/composer_json_linker.rb +++ b/lib/gitlab/dependency_linker/composer_json_linker.rb @@ -8,8 +8,8 @@ module Gitlab private def link_packages - link_packages_at_key("require", &method(:package_url)) - link_packages_at_key("require-dev", &method(:package_url)) + link_packages_at_key("require") + link_packages_at_key("require-dev") end def package_url(name) diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb index 8ab219c4962..c6e02248b0a 100644 --- a/lib/gitlab/dependency_linker/gemfile_linker.rb +++ b/lib/gitlab/dependency_linker/gemfile_linker.rb @@ -3,8 +3,14 @@ module Gitlab module DependencyLinker class GemfileLinker < MethodLinker + class_attribute :package_keyword + + self.package_keyword = :gem self.file_type = :gemfile + GITHUB_REGEX = /(github:|:github\s*=>)\s*['"](?[^'"]+)['"]/.freeze + GIT_REGEX = /(git:|:git\s*=>)\s*['"](?#{URL_REGEX})['"]/.freeze + private def link_dependencies @@ -14,21 +20,35 @@ module Gitlab def link_urls # Link `github: "user/repo"` to https://github.com/user/repo - link_regex(/(github:|:github\s*=>)\s*['"](?[^'"]+)['"]/, &method(:github_url)) + link_regex(GITHUB_REGEX, &method(:github_url)) # Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo - link_regex(/(git:|:git\s*=>)\s*['"](?#{URL_REGEX})['"]/, &:itself) + link_regex(GIT_REGEX, &:itself) # Link `source "https://rubygems.org"` to https://rubygems.org link_method_call('source', URL_REGEX, &:itself) end def link_packages - # Link `gem "package_name"` to https://rubygems.org/gems/package_name - link_method_call('gem') do |name| - "https://rubygems.org/gems/#{name}" + packages = parse_packages + + return if packages.blank? + + packages.each do |package| + link_method_call('gem', package.name) do + external_url(package.name, package.external_ref) + end end end + + def package_url(name) + "https://rubygems.org/gems/#{name}" + end + + def parse_packages + parser = Gitlab::DependencyLinker::Parser::Gemfile.new(plain_text) + parser.parse(keyword: self.class.package_keyword) + end end end end diff --git a/lib/gitlab/dependency_linker/gemspec_linker.rb b/lib/gitlab/dependency_linker/gemspec_linker.rb index b924ea86d89..94c2b375cf9 100644 --- a/lib/gitlab/dependency_linker/gemspec_linker.rb +++ b/lib/gitlab/dependency_linker/gemspec_linker.rb @@ -11,7 +11,7 @@ module Gitlab link_method_call('homepage', URL_REGEX, &:itself) link_method_call('license', &method(:license_url)) - link_method_call(%w[name add_dependency add_runtime_dependency add_development_dependency]) do |name| + link_method_call(%w[add_dependency add_runtime_dependency add_development_dependency]) do |name| "https://rubygems.org/gems/#{name}" end end diff --git a/lib/gitlab/dependency_linker/method_linker.rb b/lib/gitlab/dependency_linker/method_linker.rb index d4d85bb3390..33899a931c6 100644 --- a/lib/gitlab/dependency_linker/method_linker.rb +++ b/lib/gitlab/dependency_linker/method_linker.rb @@ -23,18 +23,22 @@ module Gitlab # link_method_call('name') # # Will link `package` in `self.name = "package"` def link_method_call(method_name, value = nil, &url_proc) + regex = method_call_regex(method_name, value) + + link_regex(regex, &url_proc) + end + + def method_call_regex(method_name, value = nil) method_name = regexp_for_value(method_name) value = regexp_for_value(value) - regex = %r{ + %r{ #{method_name} # Method name \s* # Whitespace [(=]? # Opening brace or equals sign \s* # Whitespace ['"](?#{value})['"] # Package name in quotes }x - - link_regex(regex, &url_proc) end end end diff --git a/lib/gitlab/dependency_linker/package.rb b/lib/gitlab/dependency_linker/package.rb new file mode 100644 index 00000000000..8a509bbd562 --- /dev/null +++ b/lib/gitlab/dependency_linker/package.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module DependencyLinker + class Package + attr_reader :name, :git_ref, :github_ref + + def initialize(name, git_ref, github_ref) + @name = name + @git_ref = git_ref + @github_ref = github_ref + end + + def external_ref + @git_ref || @github_ref + end + end + end +end diff --git a/lib/gitlab/dependency_linker/package_json_linker.rb b/lib/gitlab/dependency_linker/package_json_linker.rb index 578e25f806a..6857f2a4fa2 100644 --- a/lib/gitlab/dependency_linker/package_json_linker.rb +++ b/lib/gitlab/dependency_linker/package_json_linker.rb @@ -8,7 +8,6 @@ module Gitlab private def link_dependencies - link_json('name', json["name"], &method(:package_url)) link_json('license', &method(:license_url)) link_json(%w[homepage url], URL_REGEX, &:itself) @@ -16,25 +15,19 @@ module Gitlab end def link_packages - link_packages_at_key("dependencies", &method(:package_url)) - link_packages_at_key("devDependencies", &method(:package_url)) + link_packages_at_key("dependencies") + link_packages_at_key("devDependencies") end - def link_packages_at_key(key, &url_proc) + def link_packages_at_key(key) dependencies = json[key] return unless dependencies dependencies.each do |name, version| - link_json(name, version, link: :key, &url_proc) - - link_json(name) do |value| - case value - when /\A#{URL_REGEX}\z/ - value - when /\A#{REPO_REGEX}\z/ - github_url(value) - end - end + external_url = external_url(name, version) + + link_json(name, version, link: :key) { external_url } + link_json(name) { external_url } end end diff --git a/lib/gitlab/dependency_linker/parser/gemfile.rb b/lib/gitlab/dependency_linker/parser/gemfile.rb new file mode 100644 index 00000000000..7f755375cea --- /dev/null +++ b/lib/gitlab/dependency_linker/parser/gemfile.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module DependencyLinker + module Parser + class Gemfile < MethodLinker + GIT_REGEX = Gitlab::DependencyLinker::GemfileLinker::GIT_REGEX + GITHUB_REGEX = Gitlab::DependencyLinker::GemfileLinker::GITHUB_REGEX + + def initialize(plain_text) + @plain_text = plain_text + end + + # Returns a list of Gitlab::DependencyLinker::Package + # + # keyword - The package definition keyword, e.g. `:gem` for + # Gemfile parsing, `:pod` for Podfile. + def parse(keyword:) + plain_lines.each_with_object([]) do |line, packages| + name = fetch(line, method_call_regex(keyword)) + + next unless name + + git_ref = fetch(line, GIT_REGEX) + github_ref = fetch(line, GITHUB_REGEX) + + packages << Gitlab::DependencyLinker::Package.new(name, git_ref, github_ref) + end + end + + private + + def fetch(line, regex, group: :name) + match = line.match(regex) + match[group] if match + end + end + end + end +end diff --git a/lib/gitlab/dependency_linker/podfile_linker.rb b/lib/gitlab/dependency_linker/podfile_linker.rb index def9b04cca9..a20d285da79 100644 --- a/lib/gitlab/dependency_linker/podfile_linker.rb +++ b/lib/gitlab/dependency_linker/podfile_linker.rb @@ -5,12 +5,21 @@ module Gitlab class PodfileLinker < GemfileLinker include Cocoapods + self.package_keyword = :pod self.file_type = :podfile private def link_packages - link_method_call('pod', &method(:package_url)) + packages = parse_packages + + return unless packages + + packages.each do |package| + link_method_call('pod', package.name) do + external_url(package.name, package.external_ref) + end + end end end end diff --git a/lib/gitlab/dependency_linker/podspec_linker.rb b/lib/gitlab/dependency_linker/podspec_linker.rb index 6b1758c5a43..14abd3999c4 100644 --- a/lib/gitlab/dependency_linker/podspec_linker.rb +++ b/lib/gitlab/dependency_linker/podspec_linker.rb @@ -19,7 +19,7 @@ module Gitlab link_method_call('license', &method(:license_url)) link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url)) - link_method_call(%w[name dependency], &method(:package_url)) + link_method_call('dependency', &method(:package_url)) end end end diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 3edcc7ac2cd..a7aa63018fd 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -548,10 +548,7 @@ describe 'File blob', :js do it 'displays an auxiliary viewer' do aggregate_failures do # shows names of dependency manager and package - expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.') - - # shows a link to the gem - expect(page).to have_link('activerecord', href: 'https://rubygems.org/gems/activerecord') + expect(page).to have_content('This project manages its dependencies using RubyGems.') # shows a learn more link expect(page).to have_link('Learn more', href: 'https://rubygems.org/') diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb index 4d222564fd0..0ebd8994636 100644 --- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb @@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do %{#{name}} end - it 'links the module name' do - expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) + it 'does not link the module name' do + expect(subject).not_to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) end it 'links the homepage' do diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb index a97803b119e..f00f6b47b94 100644 --- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb @@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do end it 'links dependencies' do - expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails')) expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer')) - expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders')) - expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets')) expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for')) end + it 'links to external dependencies' do + expect(subject).to include(link('rails', 'https://github.com/rails/rails')) + expect(subject).to include(link('responders', 'https://github.com/rails/responders')) + expect(subject).to include(link('sprockets', 'https://gitlab.example.com/gems/sprockets')) + end + it 'links GitHub repos' do expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails')) expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders')) diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb index 24ad7d12f4c..6c6a5d70576 100644 --- a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb @@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do %{#{name}} end - it 'links the gem name' do - expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) + it 'does not link the gem name' do + expect(subject).not_to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) end it 'links the license' do diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb index 1e8b72afb7b..9050127af7f 100644 --- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb @@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do "express": "4.2.x", "bigpipe": "bigpipe/pagelet", "plates": "https://github.com/flatiron/plates/tarball/master", - "karma": "^1.4.1" + "karma": "^1.4.1", + "random": "git+https://EdOverflow@github.com/example/example.git" }, "devDependencies": { "vows": "^0.7.0", @@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do %{#{name}} end - it 'links the module name' do - expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name')) + it 'does not link the module name' do + expect(subject).not_to include(link('module-name', 'https://npmjs.com/package/module-name')) end it 'links the homepage' do @@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do expect(subject).to include(link('primus', 'https://npmjs.com/package/primus')) expect(subject).to include(link('async', 'https://npmjs.com/package/async')) expect(subject).to include(link('express', 'https://npmjs.com/package/express')) - expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe')) - expect(subject).to include(link('plates', 'https://npmjs.com/package/plates')) expect(subject).to include(link('karma', 'https://npmjs.com/package/karma')) expect(subject).to include(link('vows', 'https://npmjs.com/package/vows')) expect(subject).to include(link('assume', 'https://npmjs.com/package/assume')) expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit')) end + it 'links dependencies to URL detected on value' do + expect(subject).to include(link('bigpipe', 'https://github.com/bigpipe/pagelet')) + expect(subject).to include(link('plates', 'https://github.com/flatiron/plates/tarball/master')) + end + + it 'does not link to NPM when invalid git URL' do + expect(subject).not_to include(link('random', 'https://npmjs.com/package/random')) + end + it 'links GitHub repos' do expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet')) end diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb new file mode 100644 index 00000000000..f81dbcf62da --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::Parser::Gemfile do + describe '#parse' do + let(:file_content) do + <<-CONTENT.strip_heredoc + source 'https://rubygems.org' + + gem "rails", '4.2.6', github: "rails/rails" + gem 'rails-deprecated_sanitizer', '~> 1.0.3' + gem 'responders', '~> 2.0', :github => 'rails/responders' + gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets' + gem 'default_value_for', '~> 3.0.0' + CONTENT + end + + subject { described_class.new(file_content).parse(keyword: 'gem') } + + def fetch_package(name) + subject.find { |package| package.name == name } + end + + it 'returns parsed packages' do + expect(subject.size).to eq(5) + expect(subject).to all(be_a(Gitlab::DependencyLinker::Package)) + end + + it 'packages respond to name and external_ref accordingly' do + expect(fetch_package('rails')).to have_attributes(name: 'rails', + github_ref: 'rails/rails', + git_ref: nil) + + expect(fetch_package('sprockets')).to have_attributes(name: 'sprockets', + github_ref: nil, + git_ref: 'https://gitlab.example.com/gems/sprockets') + + expect(fetch_package('default_value_for')).to have_attributes(name: 'default_value_for', + github_ref: nil, + git_ref: nil) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb index cdfd7ad9826..8f1b523653e 100644 --- a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb @@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do it 'links packages' do expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) - expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar')) + end + + it 'links external packages' do + expect(subject).to include(link('Interstellar/Core', 'https://github.com/ashfurrow/Interstellar.git')) end it 'links Git repos' do diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb index ed60ab45955..bacec830103 100644 --- a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb @@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do %{#{name}} end - it 'links the gem name' do - expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) + it 'does not link the pod name' do + expect(subject).not_to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) end it 'links the license' do -- GitLab From c6bc91387765fa156dcbac15ee451dda34c1a897 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Mon, 25 Feb 2019 10:22:13 -0300 Subject: [PATCH 22/78] Raise not implemented error on BaseLinker for package_url --- lib/gitlab/dependency_linker/base_linker.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index 6b44d5b1518..ffad00fa7d7 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -45,6 +45,10 @@ module Gitlab private + def package_url(_name) + raise NotImplementedError + end + def link_dependencies raise NotImplementedError end -- GitLab From 7e83acb8a2f7fe4a0c0acd6769114e0593c677bb Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 22 Feb 2019 11:31:02 -0300 Subject: [PATCH 23/78] Prevent disclosing project milestone titles Prevent unauthorized users having access to milestone titles through autocomplete endpoint. --- .../autocomplete_sources_controller.rb | 2 ++ .../unreleased/security-issue_54789_2.yml | 5 +++ .../autocomplete_sources_controller_spec.rb | 31 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 changelogs/unreleased/security-issue_54789_2.yml diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index 9c130af8394..0e3f13045ce 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Projects::AutocompleteSourcesController < Projects::ApplicationController + before_action :authorize_read_milestone!, only: :milestones + def members render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target) end diff --git a/changelogs/unreleased/security-issue_54789_2.yml b/changelogs/unreleased/security-issue_54789_2.yml new file mode 100644 index 00000000000..8ecb72a2ae3 --- /dev/null +++ b/changelogs/unreleased/security-issue_54789_2.yml @@ -0,0 +1,5 @@ +--- +title: Do not disclose milestone titles for unauthorized users +merge_request: +author: +type: security diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb index 4bc72042710..a9a058e7e17 100644 --- a/spec/controllers/projects/autocomplete_sources_controller_spec.rb +++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb @@ -35,4 +35,35 @@ describe Projects::AutocompleteSourcesController do avatar_url: user.avatar_url) end end + + describe 'GET milestones' do + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, namespace: group) } + let!(:project_milestone) { create(:milestone, project: project) } + let!(:group_milestone) { create(:milestone, group: group) } + + before do + sign_in(user) + end + + it 'lists milestones' do + group.add_owner(user) + + get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path } + + milestone_titles = json_response.map { |milestone| milestone["title"] } + expect(milestone_titles).to match_array([project_milestone.title, group_milestone.title]) + end + + context 'when user cannot read project issues and merge requests' do + it 'renders 404' do + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + + get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path } + + expect(response).to have_gitlab_http_status(404) + end + end + end end -- GitLab From 725dfc77c2ed241121666c8a708efb09122c287f Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Mon, 25 Feb 2019 11:12:01 -0700 Subject: [PATCH 24/78] Adding tooltip to the icon partial I apparently forgot to add tooltips to the partial before. --- app/views/ci/status/_icon.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index 29ce9cd1afd..09ac086fa22 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -1,7 +1,7 @@ - status = local_assigns.fetch(:status) - size = local_assigns.fetch(:size, 16) - link = local_assigns.fetch(:link, true) -- title = local_assigns.fetch(:title, nil) +- title = local_assigns.fetch(:title, "Pipeline: #{status.label}") - css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} #{'has-tooltip' if title.present?}" - if link && status.has_details? -- GitLab From 506ac78d48b6a96081b02818c432650b8508913a Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Mon, 25 Feb 2019 11:25:00 -0700 Subject: [PATCH 25/78] Adding tooltip placement Adding the ability to position the tooltip of the status icon. --- app/views/ci/status/_icon.html.haml | 15 ++++++++------- .../projects/issues/_merge_requests.html.haml | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index 09ac086fa22..1fd34eacdf7 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -1,12 +1,13 @@ -- status = local_assigns.fetch(:status) -- size = local_assigns.fetch(:size, 16) -- link = local_assigns.fetch(:link, true) -- title = local_assigns.fetch(:title, "Pipeline: #{status.label}") -- css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} #{'has-tooltip' if title.present?}" +- status = local_assigns.fetch(:status) +- size = local_assigns.fetch(:size, 16) +- link = local_assigns.fetch(:link, true) +- title = local_assigns.fetch(:title, "Pipeline: #{status.label}") +- tooltip_placement = local_assigns.fetch(:tooltip_placement, "left") +- css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} #{'has-tooltip' if title.present?}" - if link && status.has_details? - = link_to status.details_path, class: css_classes, title: title, data: { html: title.present? } do + = link_to status.details_path, class: css_classes, title: title, data: { html: title.present?, placement: tooltip_placement } do = sprite_icon(status.icon, size: size) - else - %span{ class: css_classes, title: title, data: { html: title.present? } } + %span{ class: css_classes, title: title, data: { html: title.present?, placement: tooltip_placement } } = sprite_icon(status.icon, size: size) diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index ab3d7907ad8..7468217418d 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -27,7 +27,7 @@ = merge_request.to_reference %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2 - if merge_request.can_read_pipeline? - = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), link: true + = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), link: true, tooltip_placement: 'bottom' - elsif has_any_head_pipeline = icon('blank fw') -- GitLab From 4537fbb87e9bc1edd0eaaf34d5b361009c056a7a Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Tue, 26 Feb 2019 01:09:01 -0300 Subject: [PATCH 26/78] Add changelog --- .../unreleased/security-osw-stop-linking-to-packages.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/security-osw-stop-linking-to-packages.yml diff --git a/changelogs/unreleased/security-osw-stop-linking-to-packages.yml b/changelogs/unreleased/security-osw-stop-linking-to-packages.yml new file mode 100644 index 00000000000..078f06140fe --- /dev/null +++ b/changelogs/unreleased/security-osw-stop-linking-to-packages.yml @@ -0,0 +1,5 @@ +--- +title: Stop linking to unrecognized package sources +merge_request: 55518 +author: +type: security -- GitLab From 2b51745394e8568cf91ce6ee95574f84fc38722e Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Tue, 26 Feb 2019 10:59:41 -0700 Subject: [PATCH 27/78] Removed other deprecated status helpers Updating a couple other places that were still using deprecated status. --- app/helpers/ci_status_helper.rb | 37 ------------------- app/views/ci/status/_icon.html.haml | 13 +++---- app/views/projects/commits/_commit.html.haml | 4 +- .../projects/issues/_merge_requests.html.haml | 2 +- .../issues/_related_branches.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- app/views/shared/projects/_project.html.haml | 2 +- 7 files changed, 11 insertions(+), 51 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index dfeeecf1228..c02f3707b9e 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -100,43 +100,6 @@ module CiStatusHelper "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}" end - def render_project_pipeline_status(pipeline_status, tooltip_placement: 'left') - project = pipeline_status.project - path = pipelines_project_commit_path(project, pipeline_status.sha, ref: pipeline_status.ref) - - render_status_with_link( - 'commit', - pipeline_status.status, - path, - tooltip_placement: tooltip_placement) - end - - def render_commit_status(commit, ref: nil, tooltip_placement: 'left') - project = commit.project - path = pipelines_project_commit_path(project, commit, ref: ref) - - render_status_with_link( - 'commit', - commit.status(ref), - path, - tooltip_placement: tooltip_placement, - icon_size: 24) - end - - def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) - klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" - title = "#{type.titleize}: #{ci_label_for_status(status)}" - data = { toggle: 'tooltip', placement: tooltip_placement, container: container } - - if path - link_to ci_icon_for_status(status, size: icon_size), path, - class: klass, title: title, data: data - else - content_tag :span, ci_icon_for_status(status, size: icon_size), - class: klass, title: title, data: data - end - end - def detailed_status?(status) status.respond_to?(:text) && status.respond_to?(:label) && diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index 1fd34eacdf7..1fcbd9e7545 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -1,13 +1,10 @@ - status = local_assigns.fetch(:status) - size = local_assigns.fetch(:size, 16) -- link = local_assigns.fetch(:link, true) -- title = local_assigns.fetch(:title, "Pipeline: #{status.label}") +- type = local_assigns.fetch(:type, 'pipeline') +- title = local_assigns.fetch(:title, "#{type.titleize}: #{status.label}") - tooltip_placement = local_assigns.fetch(:tooltip_placement, "left") -- css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} #{'has-tooltip' if title.present?}" +- css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} has-tooltip" -- if link && status.has_details? - = link_to status.details_path, class: css_classes, title: title, data: { html: title.present?, placement: tooltip_placement } do - = sprite_icon(status.icon, size: size) -- else - %span{ class: css_classes, title: title, data: { html: title.present?, placement: tooltip_placement } } +- if status.has_details? + = link_to status.details_path, class: css_classes, title: title, data: { html: true, placement: tooltip_placement } do = sprite_icon(status.icon, size: size) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 0d3c6e7027c..1d7890f1c47 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -25,7 +25,7 @@ = commit.short_id - if commit_status .d-block.d-sm-none - = render_commit_status(commit, ref: ref) + = render 'ci/status/icon', status: commit.last_pipeline.detailed_status(current_user), type: 'commit', size: 24 - if commit.description? %button.text-expander.js-toggle-button = sprite_icon('ellipsis_h', size: 12) @@ -47,7 +47,7 @@ = render partial: 'projects/commit/ajax_signature', locals: { commit: commit } - if commit_status - = render_commit_status(commit, ref: ref) + = render 'ci/status/icon', status: commit.last_pipeline.detailed_status(current_user), type: 'commit', size: 24 .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } } diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 7468217418d..6a66c2e57cc 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -27,7 +27,7 @@ = merge_request.to_reference %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2 - if merge_request.can_read_pipeline? - = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), link: true, tooltip_placement: 'bottom' + = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), tooltip_placement: 'bottom' - elsif has_any_head_pipeline = icon('blank fw') diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index f8d1d64100a..6da4956a036 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -8,7 +8,7 @@ - pipeline = @project.pipeline_for(branch, target.sha) if target - if can?(current_user, :read_pipeline, pipeline) %span.related-branch-ci-status - = render 'ci/status/icon', status: pipeline.detailed_status(current_user), link: true + = render 'ci/status/icon', status: pipeline.detailed_status(current_user) %span.related-branch-info %strong = link_to branch, project_compare_path(@project, from: @project.default_branch, to: branch), class: "ref-name" diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 4111e823701..bfa5a471886 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -48,7 +48,7 @@ CLOSED - if can?(current_user, :read_pipeline, merge_request.head_pipeline) %li.issuable-pipeline-status.d-none.d-sm-inline-block - = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), link: true + = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user) - if merge_request.open? && merge_request.broken? %li.issuable-pipeline-broken.d-none.d-sm-inline-block = link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index df17ae95e2a..2a38dce4896 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -85,6 +85,6 @@ = number_with_delimiter(project.open_issues_count) - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project) %span.icon-wrapper.pipeline-status - = render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top') + = render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), type: 'commit', tooltip_placement: 'top' .updated-note %span Updated #{updated_tooltip} -- GitLab From 40d15136352958206685197d4176f15781089849 Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Tue, 26 Feb 2019 14:10:24 -0700 Subject: [PATCH 28/78] Adding ability to pass in path to status icon Project passed a very specific details path. Also reverted a change. --- app/helpers/ci_status_helper.rb | 26 ++++++++++++++++++++ app/views/ci/status/_icon.html.haml | 6 ++++- app/views/projects/commits/_commit.html.haml | 4 +-- app/views/shared/projects/_project.html.haml | 3 ++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index c02f3707b9e..355b91a8661 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -100,6 +100,32 @@ module CiStatusHelper "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}" end + def render_commit_status(commit, ref: nil, tooltip_placement: 'left') + project = commit.project + path = pipelines_project_commit_path(project, commit, ref: ref) + + render_status_with_link( + 'commit', + commit.status(ref), + path, + tooltip_placement: tooltip_placement, + icon_size: 24) + end + + def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) + klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" + title = "#{type.titleize}: #{ci_label_for_status(status)}" + data = { toggle: 'tooltip', placement: tooltip_placement, container: container } + + if path + link_to ci_icon_for_status(status, size: icon_size), path, + class: klass, title: title, data: data + else + content_tag :span, ci_icon_for_status(status, size: icon_size), + class: klass, title: title, data: data + end + end + def detailed_status?(status) status.respond_to?(:text) && status.respond_to?(:label) && diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index 1fcbd9e7545..f406eb4b5a3 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -3,8 +3,12 @@ - type = local_assigns.fetch(:type, 'pipeline') - title = local_assigns.fetch(:title, "#{type.titleize}: #{status.label}") - tooltip_placement = local_assigns.fetch(:tooltip_placement, "left") +- path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil) - css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} has-tooltip" - if status.has_details? - = link_to status.details_path, class: css_classes, title: title, data: { html: true, placement: tooltip_placement } do + = link_to path, class: css_classes, title: title, data: { html: true, placement: tooltip_placement } do + = sprite_icon(status.icon, size: size) +- else + %span{ class: css_classes, title: title, data: { html: true, placement: tooltip_placement } } = sprite_icon(status.icon, size: size) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1d7890f1c47..0d3c6e7027c 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -25,7 +25,7 @@ = commit.short_id - if commit_status .d-block.d-sm-none - = render 'ci/status/icon', status: commit.last_pipeline.detailed_status(current_user), type: 'commit', size: 24 + = render_commit_status(commit, ref: ref) - if commit.description? %button.text-expander.js-toggle-button = sprite_icon('ellipsis_h', size: 12) @@ -47,7 +47,7 @@ = render partial: 'projects/commit/ajax_signature', locals: { commit: commit } - if commit_status - = render 'ci/status/icon', status: commit.last_pipeline.detailed_status(current_user), type: 'commit', size: 24 + = render_commit_status(commit, ref: ref) .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } } diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 2a38dce4896..8ff676c3cf8 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -84,7 +84,8 @@ = sprite_icon('issues', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_issues_count) - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project) + - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref) %span.icon-wrapper.pipeline-status - = render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), type: 'commit', tooltip_placement: 'top' + = render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), type: 'commit', tooltip_placement: 'top', path: pipeline_path .updated-note %span Updated #{updated_tooltip} -- GitLab From 12459e507dc853239c70a1ef302de01c5a682542 Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Tue, 26 Feb 2019 15:26:01 -0700 Subject: [PATCH 29/78] Refactoring and i18n fixes Making these changes based on MR suggestions. --- app/views/ci/status/_icon.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index f406eb4b5a3..64119d73e03 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -1,12 +1,12 @@ - status = local_assigns.fetch(:status) - size = local_assigns.fetch(:size, 16) - type = local_assigns.fetch(:type, 'pipeline') -- title = local_assigns.fetch(:title, "#{type.titleize}: #{status.label}") +- title = local_assigns.fetch(:title, _("%{type}: %{label}" % {type: type.titleize, label: status.label})) - tooltip_placement = local_assigns.fetch(:tooltip_placement, "left") - path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil) - css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} has-tooltip" -- if status.has_details? +- if path = link_to path, class: css_classes, title: title, data: { html: true, placement: tooltip_placement } do = sprite_icon(status.icon, size: size) - else -- GitLab From 0ea350965d2a0fea0dbc1cfa433729dc865acfc6 Mon Sep 17 00:00:00 2001 From: Rajat Jain Date: Fri, 22 Feb 2019 13:42:25 +0530 Subject: [PATCH 30/78] Limit number of characters allowed in mermaidjs --- .../behaviors/markdown/render_mermaid.js | 19 +++++++++++++++++++ changelogs/unreleased/security-mermaid.yml | 5 +++++ locale/gitlab.pot | 3 +++ 3 files changed, 27 insertions(+) create mode 100644 changelogs/unreleased/security-mermaid.yml diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index 35380ca49fb..798114b4b0b 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -1,4 +1,5 @@ import flash from '~/flash'; +import { sprintf, __ } from '../../locale'; // Renders diagrams and flowcharts from text using Mermaid in any element with the // `js-render-mermaid` class. @@ -14,6 +15,9 @@ import flash from '~/flash'; // // +// This is an arbitary number; Can be iterated upon when suitable. +const MAX_CHAR_LIMIT = 5000; + export default function renderMermaid($els) { if (!$els.length) return; @@ -34,6 +38,21 @@ export default function renderMermaid($els) { $els.each((i, el) => { const source = el.textContent; + /** + * Restrict the rendering to a certain amount of character to + * prevent mermaidjs from hanging up the entire thread and + * causing a DoS. + */ + if (source && source.length > MAX_CHAR_LIMIT) { + el.textContent = sprintf( + __( + 'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.', + ), + { charLimit: MAX_CHAR_LIMIT }, + ); + return; + } + // Remove any extra spans added by the backend syntax highlighting. Object.assign(el, { textContent: source }); diff --git a/changelogs/unreleased/security-mermaid.yml b/changelogs/unreleased/security-mermaid.yml new file mode 100644 index 00000000000..ec42b5a1615 --- /dev/null +++ b/changelogs/unreleased/security-mermaid.yml @@ -0,0 +1,5 @@ +--- +title: Limit mermaid rendering to 5K characters +merge_request: +author: +type: security diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fa680cd5ef0..52520b467e4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1299,6 +1299,9 @@ msgstr "" msgid "Cannot modify managed Kubernetes cluster" msgstr "" +msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded." +msgstr "" + msgid "Certificate" msgstr "" -- GitLab From 44c4aad983570ea1832aa08c39f46dbc1b475fd3 Mon Sep 17 00:00:00 2001 From: Imre Farkas Date: Sat, 23 Feb 2019 19:18:44 +0100 Subject: [PATCH 31/78] Filter active sessions belonging to an admin impersonating the user --- .../profiles/active_sessions_controller.rb | 2 +- app/models/active_session.rb | 6 +++-- .../features/profiles/active_sessions_spec.rb | 23 +++++++++++++++++++ spec/models/active_session_spec.rb | 5 +++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb index efe7ede5efa..6cf7a120449 100644 --- a/app/controllers/profiles/active_sessions_controller.rb +++ b/app/controllers/profiles/active_sessions_controller.rb @@ -2,7 +2,7 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController def index - @sessions = ActiveSession.list(current_user) + @sessions = ActiveSession.list(current_user).reject(&:is_impersonated) end def destroy diff --git a/app/models/active_session.rb b/app/models/active_session.rb index 0d9c6a4a1f0..1e01f1d17e6 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -5,7 +5,8 @@ class ActiveSession attr_accessor :created_at, :updated_at, :session_id, :ip_address, - :browser, :os, :device_name, :device_type + :browser, :os, :device_name, :device_type, + :is_impersonated def current?(session) return false if session_id.nil? || session.id.nil? @@ -31,7 +32,8 @@ class ActiveSession device_type: client.device_type, created_at: user.current_sign_in_at || timestamp, updated_at: timestamp, - session_id: session_id + session_id: session_id, + is_impersonated: request.session[:impersonator_id].present? ) redis.pipelined do diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb index d3050760c06..3fd3222fa0a 100644 --- a/spec/features/profiles/active_sessions_spec.rb +++ b/spec/features/profiles/active_sessions_spec.rb @@ -7,6 +7,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do end end + let(:admin) { create(:admin) } + around do |example| Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do example.run @@ -16,6 +18,7 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do it 'User sees their active sessions' do Capybara::Session.new(:session1) Capybara::Session.new(:session2) + Capybara::Session.new(:session3) # note: headers can only be set on the non-js (aka. rack-test) driver using_session :session1 do @@ -37,9 +40,27 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do gitlab_sign_in(user) end + # set an admin session impersonating the user + using_session :session3 do + Capybara.page.driver.header( + 'User-Agent', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36' + ) + + gitlab_sign_in(admin) + + visit admin_user_path(user) + + click_link 'Impersonate' + end + using_session :session1 do visit profile_active_sessions_path + expect(page).to( + have_selector('ul.list-group li.list-group-item', { text: 'Signed in on', + count: 2 })) + expect(page).to have_content( '127.0.0.1 ' \ 'This is your current session ' \ @@ -57,6 +78,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do ) expect(page).to have_selector '[title="Smartphone"]', count: 1 + + expect(page).not_to have_content('Chrome on Windows') end end diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb index 129b2f92683..e128fe8a4b7 100644 --- a/spec/models/active_session_spec.rb +++ b/spec/models/active_session_spec.rb @@ -7,7 +7,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do end end - let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') } + let(:session) do + double(:session, { id: '6919a6f1bb119dd7396fadc38fd18d0d', + '[]': {} }) + end let(:request) do double(:request, { -- GitLab From 038d530565bc64729706bbd9afad275699be459d Mon Sep 17 00:00:00 2001 From: Imre Farkas Date: Mon, 25 Feb 2019 14:52:40 +0100 Subject: [PATCH 32/78] Remove ability to revoke active session Session ID is used as a parameter for the revoke session endpoint but it should never be included in the HTML as an attacker could obtain it via XSS. --- .../profiles/active_sessions_controller.rb | 9 ------ .../active_sessions/_active_session.html.haml | 6 ---- .../57534_filter_impersonated_sessions.yml | 6 ++++ doc/user/profile/active_sessions.md | 8 +----- doc/user/profile/img/active_sessions_list.png | Bin 22266 -> 19360 bytes .../features/profiles/active_sessions_spec.rb | 27 ------------------ 6 files changed, 7 insertions(+), 49 deletions(-) create mode 100644 changelogs/unreleased/57534_filter_impersonated_sessions.yml diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb index 6cf7a120449..c473023cacb 100644 --- a/app/controllers/profiles/active_sessions_controller.rb +++ b/app/controllers/profiles/active_sessions_controller.rb @@ -4,13 +4,4 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController def index @sessions = ActiveSession.list(current_user).reject(&:is_impersonated) end - - def destroy - ActiveSession.destroy(current_user, params[:id]) - - respond_to do |format| - format.html { redirect_to profile_active_sessions_url, status: :found } - format.js { head :ok } - end - end end diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml index 23ef31a0c85..2bf514d72a5 100644 --- a/app/views/profiles/active_sessions/_active_session.html.haml +++ b/app/views/profiles/active_sessions/_active_session.html.haml @@ -23,9 +23,3 @@ %strong Signed in on = l(active_session.created_at, format: :short) - - - unless is_current_session - .float-right - = link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do - %span.sr-only Revoke - Revoke diff --git a/changelogs/unreleased/57534_filter_impersonated_sessions.yml b/changelogs/unreleased/57534_filter_impersonated_sessions.yml new file mode 100644 index 00000000000..80aea0ab1bc --- /dev/null +++ b/changelogs/unreleased/57534_filter_impersonated_sessions.yml @@ -0,0 +1,6 @@ +--- +title: Do not display impersonated sessions under active sessions and remove ability + to revoke session +merge_request: +author: +type: security diff --git a/doc/user/profile/active_sessions.md b/doc/user/profile/active_sessions.md index 5119c0e30d0..28e3f4904a9 100644 --- a/doc/user/profile/active_sessions.md +++ b/doc/user/profile/active_sessions.md @@ -4,7 +4,7 @@ > in GitLab 10.8. GitLab lists all devices that have logged into your account. This allows you to -review the sessions and revoke any of it that you don't recognize. +review the sessions. ## Listing all active sessions @@ -12,9 +12,3 @@ review the sessions and revoke any of it that you don't recognize. 1. Navigate to the **Active Sessions** tab. ![Active sessions list](img/active_sessions_list.png) - -## Revoking a session - -1. Navigate to your [profile's](#profile-settings) **Settings > Active Sessions**. -1. Click on **Revoke** besides a session. The current session cannot be - revoked, as this would sign you out of GitLab. diff --git a/doc/user/profile/img/active_sessions_list.png b/doc/user/profile/img/active_sessions_list.png index 5d94dca69ccfded5543a6d1fc7413230faab0018..1e242ac47103c93d3f857db3ade6a05525317cb9 100644 GIT binary patch literal 19360 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 L)z4*}Q$iB}(TNx& literal 22266 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 Pl&FYB#(H9a)`t$o?7`4{Z!d*@56w%X@w&%G>e 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 gXv`1)Pt-F!FVdQ&MBb@0FcgTs{jB1 diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb index 3fd3222fa0a..2aa0177af5d 100644 --- a/spec/features/profiles/active_sessions_spec.rb +++ b/spec/features/profiles/active_sessions_spec.rb @@ -82,31 +82,4 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do expect(page).not_to have_content('Chrome on Windows') end end - - it 'User can revoke a session', :js, :redis_session_store do - Capybara::Session.new(:session1) - Capybara::Session.new(:session2) - - # set an additional session in another browser - using_session :session2 do - gitlab_sign_in(user) - end - - using_session :session1 do - gitlab_sign_in(user) - visit profile_active_sessions_path - - expect(page).to have_link('Revoke', count: 1) - - accept_confirm { click_on 'Revoke' } - - expect(page).not_to have_link('Revoke') - end - - using_session :session2 do - visit profile_active_sessions_path - - expect(page).to have_content('You need to sign in or sign up before continuing.') - end - end end -- GitLab From 871ca1e51f791c2d8b36830d521e90577608adf0 Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Wed, 27 Feb 2019 09:21:42 -0700 Subject: [PATCH 33/78] Adjusting internationalization of tooltip title Based on MR comments, changing how the i18n works for the tooltip title. --- app/views/ci/status/_icon.html.haml | 4 +++- locale/gitlab.pot | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index 64119d73e03..1ca0a55b156 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -1,10 +1,12 @@ - status = local_assigns.fetch(:status) - size = local_assigns.fetch(:size, 16) - type = local_assigns.fetch(:type, 'pipeline') -- title = local_assigns.fetch(:title, _("%{type}: %{label}" % {type: type.titleize, label: status.label})) - tooltip_placement = local_assigns.fetch(:tooltip_placement, "left") - path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil) - css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} has-tooltip" +- title = s_("PipelineStatusTooltip|Pipeline: %{ci_status}") % {ci_status: status.label} +- if type == 'commit' + - title = s_("PipelineStatusTooltip|Commit: %{ci_status}") % {ci_status: status.label} - if path = link_to path, class: css_classes, title: title, data: { html: true, placement: tooltip_placement } do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 973950d27aa..ef063451c4f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5362,6 +5362,12 @@ msgstr "" msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" +msgid "PipelineStatusTooltip|Commit: %{ci_status}" +msgstr "" + +msgid "PipelineStatusTooltip|Pipeline: %{ci_status}" +msgstr "" + msgid "Pipelines" msgstr "" -- GitLab From 99b2a5a2ebcf878b09e0591a7e4c39fe533e8e74 Mon Sep 17 00:00:00 2001 From: Scott Hampton Date: Wed, 27 Feb 2019 12:59:53 -0700 Subject: [PATCH 34/78] Remove extraneous data attribute I had `html: true` as a data attribute, but the tooltip was just text. --- app/views/ci/status/_icon.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index 1ca0a55b156..f38bdb2e5ed 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -9,8 +9,8 @@ - title = s_("PipelineStatusTooltip|Commit: %{ci_status}") % {ci_status: status.label} - if path - = link_to path, class: css_classes, title: title, data: { html: true, placement: tooltip_placement } do + = link_to path, class: css_classes, title: title, data: { placement: tooltip_placement } do = sprite_icon(status.icon, size: size) - else - %span{ class: css_classes, title: title, data: { html: true, placement: tooltip_placement } } + %span{ class: css_classes, title: title, data: { placement: tooltip_placement } } = sprite_icon(status.icon, size: size) -- GitLab From 4c887a1975e453afe27e536d73175e2d89f6a8b5 Mon Sep 17 00:00:00 2001 From: GitLab Release Tools Bot Date: Thu, 28 Feb 2019 12:57:33 +0000 Subject: [PATCH 35/78] Update CHANGELOG.md for 11.6.10 [ci skip] --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index feda5e0835b..8a4a6c9ff13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -486,6 +486,33 @@ entry. - Update url placeholder for the sentry configuration page. !24338 +## 11.6.10 (2019-02-28) + +### Security (21 changes) + +- Stop linking to unrecognized package sources. !55518 +- Check snippet attached file to be moved is within designated directory. +- Fix potential Addressable::URI::InvalidURIError. +- Do not display impersonated sessions under active sessions and remove ability to revoke session. +- Display only information visible to current user on the Milestone page. +- Show only merge requests visible to user on milestone detail page. +- Disable issue boards API when issues are disabled. +- Don't show new issue link after move when a user does not have permissions. +- Fix git clone revealing private repo's presence. +- Fix blind SSRF in Prometheus integration by checking URL before querying. +- Check if desired milestone for an issue is available. +- Don't allow non-members to see private related MRs. +- Fix arbitrary file read via diffs during import. +- Display the correct number of MRs a user has access to. +- Forbid creating discussions for users with restricted access. +- Do not disclose milestone titles for unauthorized users. +- Validate session key when authorizing with GCP to create a cluster. +- Block local URLs for Kubernetes integration. +- Limit mermaid rendering to 5K characters. +- Remove the possibility to share a project with a group that a user is not a member of. +- Fix leaking private repository information in API. + + ## 11.6.8 (2019-01-30) - No changes. -- GitLab From 3a321c80031630c3687cfdc08699bb0824a3dbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C5=82gorzata=20Ksionek?= Date: Mon, 11 Feb 2019 12:53:58 +0100 Subject: [PATCH 36/78] Secure vulerability and add specs --- app/policies/group_policy.rb | 1 - .../security-shared-project-private-group.yml | 5 +++ .../projects/group_links_controller_spec.rb | 2 + .../security/group/private_access_spec.rb | 32 +++++++++++++-- spec/policies/group_policy_spec.rb | 40 ++++++++++++++++--- 5 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/security-shared-project-private-group.yml diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index c25766a5af8..55d8da78dc8 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -53,7 +53,6 @@ class GroupPolicy < BasePolicy rule { admin }.enable :read_group rule { has_projects }.policy do - enable :read_group enable :read_label end diff --git a/changelogs/unreleased/security-shared-project-private-group.yml b/changelogs/unreleased/security-shared-project-private-group.yml new file mode 100644 index 00000000000..3b21daa5491 --- /dev/null +++ b/changelogs/unreleased/security-shared-project-private-group.yml @@ -0,0 +1,5 @@ +--- +title: Fixed ability to see private groups by users not belonging to given group +merge_request: +author: +type: security diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 675eeff8d12..b985a9f2846 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -67,6 +67,8 @@ describe Projects::GroupLinksController do context 'when project group id equal link group id' do before do + group2.add_developer(user) + post(:create, params: { namespace_id: project.namespace, project_id: project, diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb index 4705cd12d23..3238e07fe15 100644 --- a/spec/features/security/group/private_access_spec.rb +++ b/spec/features/security/group/private_access_spec.rb @@ -27,7 +27,7 @@ describe 'Private Group access' do it { is_expected.to be_allowed_for(:developer).of(group) } it { is_expected.to be_allowed_for(:reporter).of(group) } it { is_expected.to be_allowed_for(:guest).of(group) } - it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(project_guest) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } @@ -42,7 +42,7 @@ describe 'Private Group access' do it { is_expected.to be_allowed_for(:developer).of(group) } it { is_expected.to be_allowed_for(:reporter).of(group) } it { is_expected.to be_allowed_for(:guest).of(group) } - it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(project_guest) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } @@ -58,7 +58,7 @@ describe 'Private Group access' do it { is_expected.to be_allowed_for(:developer).of(group) } it { is_expected.to be_allowed_for(:reporter).of(group) } it { is_expected.to be_allowed_for(:guest).of(group) } - it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(project_guest) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } @@ -73,7 +73,7 @@ describe 'Private Group access' do it { is_expected.to be_allowed_for(:developer).of(group) } it { is_expected.to be_allowed_for(:reporter).of(group) } it { is_expected.to be_allowed_for(:guest).of(group) } - it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(project_guest) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } @@ -93,4 +93,28 @@ describe 'Private Group access' do it { is_expected.to be_denied_for(:visitor) } it { is_expected.to be_denied_for(:external) } end + + describe 'GET /groups/:path for shared projects' do + let(:project) { create(:project, :public) } + before do + Projects::GroupLinks::CreateService.new( + project, + create(:user), + link_group_access: ProjectGroupLink::DEVELOPER + ).execute(group) + end + + subject { group_path(group) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:maintainer).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_denied_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index be1804c5ce0..4c31ff30fc6 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -74,6 +74,38 @@ describe GroupPolicy do end end + context 'with no user and public project' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:current_user) { nil } + + before do + Projects::GroupLinks::CreateService.new( + project, + user, + link_group_access: ProjectGroupLink::DEVELOPER + ).execute(group) + end + + it { expect_disallowed(:read_group) } + end + + context 'with foreign user and public project' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:current_user) { create(:user) } + + before do + Projects::GroupLinks::CreateService.new( + project, + user, + link_group_access: ProjectGroupLink::DEVELOPER + ).execute(group) + end + + it { expect_disallowed(:read_group) } + end + context 'has projects' do let(:current_user) { create(:user) } let(:project) { create(:project, namespace: group) } @@ -82,17 +114,13 @@ describe GroupPolicy do project.add_developer(current_user) end - it do - expect_allowed(:read_group, :read_label) - end + it { expect_allowed(:read_label) } context 'in subgroups', :nested_groups do let(:subgroup) { create(:group, :private, parent: group) } let(:project) { create(:project, namespace: subgroup) } - it do - expect_allowed(:read_group, :read_label) - end + it { expect_allowed(:read_label) } end end -- GitLab From e915d962f0217895c7eeb5e4cdf31398dfd15d2a Mon Sep 17 00:00:00 2001 From: caleb Date: Sun, 3 Mar 2019 20:06:35 -0600 Subject: [PATCH 37/78] updated health check typo Removed the `/-/health/` typo (results in 404) and adjusted some formatting. --- doc/user/admin_area/monitoring/health_check.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md index c22982ac190..8d5113ac469 100644 --- a/doc/user/admin_area/monitoring/health_check.md +++ b/doc/user/admin_area/monitoring/health_check.md @@ -16,21 +16,19 @@ traffic until the system is ready or restart the container as needed. ## IP whitelist -To access monitoring resources, the client IP needs to be included in a whitelist. +To access monitoring resources, the requesting client IP needs to be included in a whitelist. [Read how to add IPs to a whitelist for the monitoring endpoints][admin]. ## Using the endpoints -With default whitelist settings, the probes can be accessed from localhost: +With default whitelist settings, the probes can be accessed from localhost using the following format: - `http://localhost/-/health` - `http://localhost/-/readiness` - `http://localhost/-/liveness` -The first endpoint, `/-/health/`, only checks whether the application server is running. It does --not verify the database or other services are running. A successful response will return -a 200 status code with the following message: +The first endpoint, `health`, only checks whether the application server is running. It does not verify the database or other services are running. A successful response will return a 200 status code with the following message: ``` GitLab OK @@ -38,9 +36,9 @@ GitLab OK The readiness and liveness probes will provide a report of system health in JSON format. -Readiness example output: +`Readiness` probe example output: -``` +```json { "queues_check" : { "status" : "ok" @@ -60,9 +58,9 @@ Readiness example output: } ``` -Liveness example output: +`Liveness` probe example output: -``` +```json { "cache_check" : { "status" : "ok" -- GitLab From 27417ccf3cd4c6e9318f8671c02400352dbdaa9f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 25 Feb 2019 14:06:52 +0100 Subject: [PATCH 38/78] Add Serverless CI/CD template --- .../ci/templates/Serverless.gitlab-ci.yml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 lib/gitlab/ci/templates/Serverless.gitlab-ci.yml diff --git a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml new file mode 100644 index 00000000000..3f7a8190e99 --- /dev/null +++ b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml @@ -0,0 +1,39 @@ +# GitLab Serverless template + +image: alpine:latest + +stages: + - build + - deploy + +.serverless:build:dockerfile: + variables: + DOCKERFILE: "Dockerfile" + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + only: + - master + script: + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/$DOCKERFILE --destination $CI_REGISTRY_IMAGE + +.serverless:deploy:image: + stage: deploy + image: gcr.io/triggermesh/tm@sha256:e3ee74db94d215bd297738d93577481f3e4db38013326c90d57f873df7ab41d5 + only: + - master + environment: development + script: + - echo "$CI_REGISTRY_IMAGE" + - tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait + +.serverless:deploy:functions: + stage: deploy + environment: development + image: gcr.io/triggermesh/tm:v0.0.9 + script: + - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" --push + - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_DEPLOY_USER" --password "$CI_DEPLOY_PASSWORD" --pull + - tm -n "$KUBE_NAMESPACE" deploy --wait -- GitLab From f5579abc9aa059b7820dea5b66ead10ce4118b89 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 26 Feb 2019 12:15:20 +0100 Subject: [PATCH 39/78] Make it possible to easily deploy serverless functions This commits add a job that is going to be triggered whenever someone adds `[serverless deploy-functions]` text into their commit message. This make it really simple to deploy functions in FaaS-only project. --- lib/gitlab/ci/templates/Serverless.gitlab-ci.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml index 3f7a8190e99..99a73826cf6 100644 --- a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml @@ -6,7 +6,7 @@ stages: - build - deploy -.serverless:build:dockerfile: +.serverless:build:image: variables: DOCKERFILE: "Dockerfile" stage: build @@ -14,7 +14,8 @@ stages: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] only: - - master + refs: + - master script: - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/$DOCKERFILE --destination $CI_REGISTRY_IMAGE @@ -23,7 +24,8 @@ stages: stage: deploy image: gcr.io/triggermesh/tm@sha256:e3ee74db94d215bd297738d93577481f3e4db38013326c90d57f873df7ab41d5 only: - - master + refs: + - master environment: development script: - echo "$CI_REGISTRY_IMAGE" @@ -37,3 +39,10 @@ stages: - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" --push - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_DEPLOY_USER" --password "$CI_DEPLOY_PASSWORD" --pull - tm -n "$KUBE_NAMESPACE" deploy --wait + +deploy-functions: + extends: .serverless:deploy:functions + environment: production + only: + variables: + - $CI_COMMIT_MESSAGE =~ /\[serverless deploy-functions\]/ -- GitLab From b1ef83914c274fd922290201ba86a4388c24e429 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 26 Feb 2019 12:25:47 +0100 Subject: [PATCH 40/78] Add changelog for serverless .gitlab-ci.yml template --- .../unreleased/feature-gb-add-serverless-cicd-template.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/feature-gb-add-serverless-cicd-template.yml diff --git a/changelogs/unreleased/feature-gb-add-serverless-cicd-template.yml b/changelogs/unreleased/feature-gb-add-serverless-cicd-template.yml new file mode 100644 index 00000000000..ebb71f00c4b --- /dev/null +++ b/changelogs/unreleased/feature-gb-add-serverless-cicd-template.yml @@ -0,0 +1,5 @@ +--- +title: Simplify CI/CD configuration on serverless projects +merge_request: 25523 +author: +type: added -- GitLab From d9fb27f42ac65b72c92fecdc2a3a292c76e9d7d7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 27 Feb 2019 18:12:28 +0100 Subject: [PATCH 41/78] Make serverless template a valid abstract template --- .../ci/templates/Serverless.gitlab-ci.yml | 7 ---- .../lib/gitlab/ci/templates/templates_spec.rb | 37 +++++++++++++++++-- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml index 99a73826cf6..4f3d08d98fe 100644 --- a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml @@ -39,10 +39,3 @@ stages: - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" --push - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_DEPLOY_USER" --password "$CI_DEPLOY_PASSWORD" --pull - tm -n "$KUBE_NAMESPACE" deploy --wait - -deploy-functions: - extends: .serverless:deploy:functions - environment: production - only: - variables: - - $CI_COMMIT_MESSAGE =~ /\[serverless deploy-functions\]/ diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index 0dd74399a47..5dad8b1ba92 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -3,9 +3,40 @@ require 'spec_helper' describe "CI YML Templates" do - Gitlab::Template::GitlabCiYmlTemplate.all.each do |template| - it "#{template.name} should be valid" do - expect { Gitlab::Ci::YamlProcessor.new(template.content) }.not_to raise_error + ABSTRACT_TEMPLATES = %w[Serverless] + + def self.concrete_templates + Gitlab::Template::GitlabCiYmlTemplate.all.reject do |template| + ABSTRACT_TEMPLATES.include?(template.name) + end + end + + def self.abstract_templates + Gitlab::Template::GitlabCiYmlTemplate.all.select do |template| + ABSTRACT_TEMPLATES.include?(template.name) + end + end + + describe 'concrete templates with CI/CD jobs' do + concrete_templates.each do |template| + it "#{template.name} template should be valid" do + expect { Gitlab::Ci::YamlProcessor.new(template.content) } + .not_to raise_error + end + end + end + + describe 'abstract templates without concrete jobs defined' do + abstract_templates.each do |template| + it "#{template.name} template should be valid after being implemented" do + content = template.content + <<~EOS + concrete_build_implemented_by_a_user: + stage: build + script: do something + EOS + + expect { Gitlab::Ci::YamlProcessor.new(content) }.not_to raise_error + end end end end -- GitLab From 714c77a6fa216f9a1aa20baa6e7ea4fe6712bb29 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 27 Feb 2019 18:25:00 +0100 Subject: [PATCH 42/78] Add basic documentation about Serverless template --- doc/user/project/clusters/serverless/index.md | 60 ++++++++----------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 2871510d5ed..2d587dd7ffe 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -114,28 +114,29 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ - Public, continue to the next step. - Private, you will need to [create a GitLab deploy token](../../deploy_tokens/index.md#creating-a-deploy-token) with `gitlab-deploy-token` as the name and the `read_registry` scope. -1. `.gitlab-ci.yml`: This template allows to define the stage, environment, and - image to be used for your functions. It must be included at the root of your repository: +1. `.gitlab-ci.yml`: this configuration allows to define the environment to be + used to deploy your functions. It must be included at the root of your repository: ```yaml - stages: - - deploy + include: + template: Serverless.gitlab-ci.yml functions: - stage: deploy - environment: test - image: gcr.io/triggermesh/tm:v0.0.9 - script: - - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" --push - - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_DEPLOY_USER" --password "$CI_DEPLOY_PASSWORD" --pull - - tm -n "$KUBE_NAMESPACE" deploy --wait - + extends: .serverless:deploy:functions + environment: production ``` - The `gitlab-ci.yml` template creates a `Deploy` stage with a `functions` job that invokes the `tm` CLI with the required parameters. + This `.gitlab-ci.yml` creates a `Deploy` stage with the `functions` job + that invokes some predefined commands to deploy your functions to Knative. + +2. `serverless.yml`: this file contains the metadata for your functions, + such as name, runtime, and environment. -2. `serverless.yml`: This file contains the metadata for your functions, - such as name, runtime, and environment. It must be included at the root of your repository. The following is a sample `echo` function which shows the required structure for the file. You can find the relevant files for this project in the [functions example project](https://gitlab.com/knative-examples/functions). + It must be included at the root of your repository. + The following is a sample `echo` function which shows the required structure + for the file. + + You can find the relevant files for this project in the [functions example project](https://gitlab.com/knative-examples/functions). ```yaml service: my-functions @@ -234,32 +235,21 @@ Add the following `.gitlab-ci.yml` to the root of your repository (you may skip this step if you've previously cloned the sample [Knative Ruby App](https://gitlab.com/knative-examples/knative-ruby-app) mentioned above): ```yaml -stages: - - build - - deploy +include: + template: Serverless.gitlab-ci.yml build: - stage: build - image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [""] - only: - - master - script: - - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE + extends: .serverless:build:image deploy: - stage: deploy - image: gcr.io/triggermesh/tm@sha256:e3ee74db94d215bd297738d93577481f3e4db38013326c90d57f873df7ab41d5 - only: - - master - environment: production - script: - - echo "$CI_REGISTRY_IMAGE" - - tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait + extends: .serverless:deploy:image ``` +`Serverless.gitlab-ci.yml` is a template that allows customization. +You can either import it with `include:` directive and use `extends` to +customize your jobs, or you can inline entire template by choosing it +from "Templates" dropdown. (TODO: link to docs about templates). + ### Deploy the application with Knative With all the pieces in place, the next time a CI pipeline runs, the Knative application will be deployed. Navigate to -- GitLab From b491e3f91b4ec6a961c37222a43ce39c009ba3b0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Feb 2019 14:26:56 +0100 Subject: [PATCH 43/78] Freeze abstract templates constant in specs --- spec/lib/gitlab/ci/templates/templates_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index 5dad8b1ba92..fbbd58280a9 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "CI YML Templates" do - ABSTRACT_TEMPLATES = %w[Serverless] + ABSTRACT_TEMPLATES = %w[Serverless].freeze def self.concrete_templates Gitlab::Template::GitlabCiYmlTemplate.all.reject do |template| -- GitLab From 21a1f0971706b51ecbecfd2ada855aba6d7454a4 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Feb 2019 14:32:15 +0100 Subject: [PATCH 44/78] Copy-edit new serverless CI/CD configuration docs --- doc/user/project/clusters/serverless/index.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 2d587dd7ffe..947e92273a4 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -114,8 +114,8 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ - Public, continue to the next step. - Private, you will need to [create a GitLab deploy token](../../deploy_tokens/index.md#creating-a-deploy-token) with `gitlab-deploy-token` as the name and the `read_registry` scope. -1. `.gitlab-ci.yml`: this configuration allows to define the environment to be - used to deploy your functions. It must be included at the root of your repository: +1. `.gitlab-ci.yml`: this defines a pipeline used to deploy your functions. + It must be included at the root of your repository: ```yaml include: @@ -126,8 +126,14 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ environment: production ``` - This `.gitlab-ci.yml` creates a `Deploy` stage with the `functions` job - that invokes some predefined commands to deploy your functions to Knative. + This `.gitlab-ci.yml` creates a `functions` job that invokes some + predefined commands to deploy your functions to Knative. + + `Serverless.gitlab-ci.yml` is a template that allows customization. + You can either import it with `include` parameter and use `extends` to + customize your jobs, or you can inline entire template by choosing it + from "Apply a template" dropdown when editing `.gitlab-ci.yml` file through + the User Interface. 2. `serverless.yml`: this file contains the metadata for your functions, such as name, runtime, and environment. @@ -246,9 +252,10 @@ deploy: ``` `Serverless.gitlab-ci.yml` is a template that allows customization. -You can either import it with `include:` directive and use `extends` to +You can either import it with `include` parameter and use `extends` to customize your jobs, or you can inline entire template by choosing it -from "Templates" dropdown. (TODO: link to docs about templates). +from "Apply a template" dropdown when editing `.gitlab-ci.yml` file through +the User Interface. ### Deploy the application with Knative -- GitLab From ae0fe4882469bc3c7ceb29bc72f12815ce648733 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 1 Mar 2019 08:08:51 +0000 Subject: [PATCH 45/78] Apply suggestion to doc/user/project/clusters/serverless/index.md --- doc/user/project/clusters/serverless/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 947e92273a4..b14fdf09f7f 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -255,7 +255,7 @@ deploy: You can either import it with `include` parameter and use `extends` to customize your jobs, or you can inline entire template by choosing it from "Apply a template" dropdown when editing `.gitlab-ci.yml` file through -the User Interface. +the user interface. ### Deploy the application with Knative -- GitLab From 73e78b7faf4d976f60a9247df11a233fe055747b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 1 Mar 2019 08:09:09 +0000 Subject: [PATCH 46/78] Apply suggestion to doc/user/project/clusters/serverless/index.md --- doc/user/project/clusters/serverless/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index b14fdf09f7f..6454155419c 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -254,7 +254,7 @@ deploy: `Serverless.gitlab-ci.yml` is a template that allows customization. You can either import it with `include` parameter and use `extends` to customize your jobs, or you can inline entire template by choosing it -from "Apply a template" dropdown when editing `.gitlab-ci.yml` file through +from **Apply a template** dropdown when editing the `.gitlab-ci.yml` file through the user interface. ### Deploy the application with Knative -- GitLab From f0ba91fd833e15c6a4cdbecea88641d966a2a498 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 1 Mar 2019 08:09:22 +0000 Subject: [PATCH 47/78] Apply suggestion to doc/user/project/clusters/serverless/index.md --- doc/user/project/clusters/serverless/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 6454155419c..190f5df31ec 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -253,7 +253,7 @@ deploy: `Serverless.gitlab-ci.yml` is a template that allows customization. You can either import it with `include` parameter and use `extends` to -customize your jobs, or you can inline entire template by choosing it +customize your jobs, or you can inline the entire template by choosing it from **Apply a template** dropdown when editing the `.gitlab-ci.yml` file through the user interface. -- GitLab From 085b8189c5041d08ec3476aa2390f518cbfa3fae Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 1 Mar 2019 08:09:32 +0000 Subject: [PATCH 48/78] Apply suggestion to doc/user/project/clusters/serverless/index.md --- doc/user/project/clusters/serverless/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 190f5df31ec..91236fd0803 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -133,7 +133,7 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ You can either import it with `include` parameter and use `extends` to customize your jobs, or you can inline entire template by choosing it from "Apply a template" dropdown when editing `.gitlab-ci.yml` file through - the User Interface. + the user interface. 2. `serverless.yml`: this file contains the metadata for your functions, such as name, runtime, and environment. -- GitLab From e2a5d369b54c42ca6e600f21dce31a5c595aedcc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 1 Mar 2019 08:09:53 +0000 Subject: [PATCH 49/78] Apply suggestion to doc/user/project/clusters/serverless/index.md --- doc/user/project/clusters/serverless/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 91236fd0803..03119c0db48 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -132,7 +132,7 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ `Serverless.gitlab-ci.yml` is a template that allows customization. You can either import it with `include` parameter and use `extends` to customize your jobs, or you can inline entire template by choosing it - from "Apply a template" dropdown when editing `.gitlab-ci.yml` file through + from **Apply a template** dropdown when editing the `.gitlab-ci.yml` file through the user interface. 2. `serverless.yml`: this file contains the metadata for your functions, -- GitLab From f4c87aa3b21b8cdead7a6b6acdebcbb0eff9bd55 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 1 Mar 2019 08:10:06 +0000 Subject: [PATCH 50/78] Apply suggestion to doc/user/project/clusters/serverless/index.md --- doc/user/project/clusters/serverless/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 03119c0db48..438e2beb059 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -131,7 +131,7 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ `Serverless.gitlab-ci.yml` is a template that allows customization. You can either import it with `include` parameter and use `extends` to - customize your jobs, or you can inline entire template by choosing it + customize your jobs, or you can inline the entire template by choosing it from **Apply a template** dropdown when editing the `.gitlab-ci.yml` file through the user interface. -- GitLab From 248fe72c6591b7ad85600817a2320f3cd7534e19 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 1 Mar 2019 08:55:51 +0000 Subject: [PATCH 51/78] Update index.md --- doc/user/project/clusters/serverless/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 438e2beb059..856ae03f4bc 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -127,7 +127,7 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ ``` This `.gitlab-ci.yml` creates a `functions` job that invokes some - predefined commands to deploy your functions to Knative. + predefined commands to deploy your functions to your cluster. `Serverless.gitlab-ci.yml` is a template that allows customization. You can either import it with `include` parameter and use `extends` to -- GitLab From c4349575aaa5899c32497595ef25337716a2ce6b Mon Sep 17 00:00:00 2001 From: caleb Date: Mon, 4 Mar 2019 09:13:06 -0600 Subject: [PATCH 52/78] adjusted per Evan R suggestions in open MR Adjusted a few different areas per Evan R's suggestions within the open MR for this branch. --- doc/user/admin_area/monitoring/health_check.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md index 8d5113ac469..a0c51739814 100644 --- a/doc/user/admin_area/monitoring/health_check.md +++ b/doc/user/admin_area/monitoring/health_check.md @@ -1,6 +1,7 @@ # Health Check > **Notes:** + > - Liveness and readiness probes were [introduced][ce-10416] in GitLab 9.1. > - The `health_check` endpoint was [introduced][ce-3888] in GitLab 8.8 and will > be deprecated in GitLab 9.1. Read more in the [old behavior](#old-behavior) @@ -22,7 +23,7 @@ To access monitoring resources, the requesting client IP needs to be included in ## Using the endpoints -With default whitelist settings, the probes can be accessed from localhost using the following format: +With default whitelist settings, the probes can be accessed from localhost using the following URLs: - `http://localhost/-/health` - `http://localhost/-/readiness` @@ -36,7 +37,7 @@ GitLab OK The readiness and liveness probes will provide a report of system health in JSON format. -`Readiness` probe example output: +`readiness` probe example output: ```json { @@ -58,7 +59,7 @@ The readiness and liveness probes will provide a report of system health in JSON } ``` -`Liveness` probe example output: +`liveness` probe example output: ```json { -- GitLab From f6350faca1a9680c3ab8f68a05f289c89a4a2272 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Tue, 29 May 2018 15:33:13 -0500 Subject: [PATCH 53/78] Allow raw `tls_options` to be passed in LDAP configuration We've previously exposed ca_file and ssl_version but there are many possible options that can be used inside tls_options. Instead of exposing individual ones, simply expose the entire hash so it can be passed in and we won't have to add things in the future. --- Gemfile | 2 +- Gemfile.lock | 4 +- .../unreleased/add_ldap_tls_options.yml | 5 + config/gitlab.yml.example | 65 ++++++-- config/initializers/1_settings.rb | 18 +++ doc/administration/auth/ldap.md | 56 ++++++- lib/gitlab/auth/ldap/config.rb | 57 +++++-- spec/lib/gitlab/auth/ldap/config_spec.rb | 153 ++++++++++++++++-- 8 files changed, 307 insertions(+), 53 deletions(-) create mode 100644 changelogs/unreleased/add_ldap_tls_options.yml diff --git a/Gemfile b/Gemfile index 0178ca9f352..03d67810faa 100644 --- a/Gemfile +++ b/Gemfile @@ -68,7 +68,7 @@ gem 'gpgme', '~> 2.0.18' # 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.4', require: 'omniauth-ldap' +gem 'gitlab_omniauth-ldap', '~> 2.1.1', require: 'omniauth-ldap' gem 'net-ldap' # API diff --git a/Gemfile.lock b/Gemfile.lock index 74a06581da2..4c92ab23a28 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -291,7 +291,7 @@ GEM rubocop (~> 0.54.0) rubocop-gitlab-security (~> 0.1.0) rubocop-rspec (~> 1.19) - gitlab_omniauth-ldap (2.0.4) + gitlab_omniauth-ldap (2.1.1) net-ldap (~> 0.16) omniauth (~> 1.3) pyu-ruby-sasl (>= 0.0.3.3, < 0.1) @@ -1024,7 +1024,7 @@ DEPENDENCIES gitlab-markup (~> 1.6.5) gitlab-sidekiq-fetcher (~> 0.4.0) gitlab-styles (~> 2.4) - gitlab_omniauth-ldap (~> 2.0.4) + gitlab_omniauth-ldap (~> 2.1.1) gon (~> 6.2) google-api-client (~> 0.23) google-protobuf (~> 3.6) diff --git a/changelogs/unreleased/add_ldap_tls_options.yml b/changelogs/unreleased/add_ldap_tls_options.yml new file mode 100644 index 00000000000..c3678cb8fb6 --- /dev/null +++ b/changelogs/unreleased/add_ldap_tls_options.yml @@ -0,0 +1,5 @@ +--- +title: Allow raw `tls_options` to be passed in LDAP configuration +merge_request: 20678 +author: +type: changed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 1a5b9ec3f02..2ad992a059c 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -379,19 +379,54 @@ production: &base # "start_tls" or "simple_tls". Defaults to true. verify_certificates: true - # 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_file: '' - - # Specifies the SSL version for OpenSSL to use, if the OpenSSL default - # is not appropriate. - # - # Example: 'TLSv1_1' - # - ssl_version: '' + # OpenSSL::SSL::SSLContext options. + tls_options: + # 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_file: '' + + # Specifies the SSL version for OpenSSL to use, if the OpenSSL default + # is not appropriate. + # + # Example: 'TLSv1_1' + # + ssl_version: '' + + # Specific SSL ciphers to use in communication with LDAP servers. + # + # Example: 'ALL:!EXPORT:!LOW:!aNULL:!eNULL:!SSLv2' + ciphers: '' + + # Client certificate + # + # Example: + # cert: | + # -----BEGIN CERTIFICATE----- + # MIIDbDCCAlSgAwIBAgIGAWkJxLmKMA0GCSqGSIb3DQEBCwUAMHcxFDASBgNVBAoTC0dvb2dsZSBJ + # bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UE + # CxMGR1N1aXRlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTAeFw0xOTAyMjAwNzE4 + # rntnF4d+0dd7zP3jrWkbdtoqjLDT/5D7NYRmVCD5vizV98FJ5//PIHbD1gL3a9b2MPAc6k7NV8tl + # ... + # 4SbuJPAiJxC1LQ0t39dR6oMCAMab3hXQqhL56LrR6cRBp6Mtlphv7alu9xb/x51y2x+g2zWtsf80 + # Jrv/vKMsIh/sAyuogb7hqMtp55ecnKxceg== + # -----END CERTIFICATE ----- + cert: '' + + # Client private key + # key: | + # -----BEGIN PRIVATE KEY----- + # MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3DmJtLRmJGY4xU1QtI3yjvxO6 + # bNuyE4z1NF6Xn7VSbcAaQtavWQ6GZi5uukMo+W5DHVtEkgDwh92ySZMuJdJogFbNvJvHAayheCdN + # 7mCQ2UUT9jGXIbmksUn9QMeJVXTZjgJWJzPXToeUdinx9G7+lpVa62UATEd1gaI3oyL72WmpDy/C + # rntnF4d+0dd7zP3jrWkbdtoqjLDT/5D7NYRmVCD5vizV98FJ5//PIHbD1gL3a9b2MPAc6k7NV8tl + # ... + # +9IhSYX+XIg7BZOVDeYqlPfxRvQh8vy3qjt/KUihmEPioAjLaGiihs1Fk5ctLk9A2hIUyP+sEQv9 + # l6RG+a/mW+0rCWn8JAd464Ps9hE= + # -----END PRIVATE KEY----- + key: '' # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking # a request if the LDAP server becomes unresponsive. @@ -653,8 +688,8 @@ production: &base # # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional # # encryption: 'AES256' # # Turns on AWS Server-Side Encryption with Amazon Customer-Provided Encryption Keys for backups, this is optional - # # This should be set to the 256-bit, base64-encoded encryption key for Amazon S3 to use to encrypt or decrypt your data. - # # 'encryption' must also be set in order for this to have any effect. + # # This should be set to the 256-bit, base64-encoded encryption key for Amazon S3 to use to encrypt or decrypt your data. + # # 'encryption' must also be set in order for this to have any effect. # # encryption_key: '' # # Specifies Amazon S3 storage class to use for backups, this is optional # # storage_class: 'STANDARD' diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 1344b3cb1f6..03800f3d9d2 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -40,6 +40,24 @@ if Settings.ldap['enabled'] || Rails.env.test? # Since GitLab 10.0, verify_certificates defaults to true for security. server['verify_certificates'] = true if server['verify_certificates'].nil? + # Expose ability to set `tls_options` directly. Deprecate `ca_file` and + # `ssl_version` in favor of `tls_options` hash option. + server['tls_options'] ||= {} + + if server['ssl_version'] || server['ca_file'] + Rails.logger.warn 'DEPRECATED: LDAP options `ssl_version` and `ca_file` should be nested within `tls_options`' + end + + if server['ssl_version'] + server['tls_options']['ssl_version'] ||= server['ssl_version'] + server.delete('ssl_version') + end + + if server['ca_file'] + server['tls_options']['ca_file'] ||= server['ca_file'] + server.delete('ca_file') + end + Settings.ldap['servers'][key] = server end end diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index d5d0d99ac24..01cf7489c09 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -136,14 +136,54 @@ main: ## verify_certificates: true - ## - ## Specifies the SSL version for OpenSSL to use, if the OpenSSL default - ## is not appropriate. - ## - ## Example: 'TLSv1_1' - ## - ## - ssl_version: '' + # OpenSSL::SSL::SSLContext options. + tls_options: + # 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_file: '' + + # Specifies the SSL version for OpenSSL to use, if the OpenSSL default + # is not appropriate. + # + # Example: 'TLSv1_1' + # + ssl_version: '' + + # Specific SSL ciphers to use in communication with LDAP servers. + # + # Example: 'ALL:!EXPORT:!LOW:!aNULL:!eNULL:!SSLv2' + ciphers: '' + + # Client certificate + # + # Example: + # cert: | + # -----BEGIN CERTIFICATE----- + # MIIDbDCCAlSgAwIBAgIGAWkJxLmKMA0GCSqGSIb3DQEBCwUAMHcxFDASBgNVBAoTC0dvb2dsZSBJ + # bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UE + # CxMGR1N1aXRlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTAeFw0xOTAyMjAwNzE4 + # rntnF4d+0dd7zP3jrWkbdtoqjLDT/5D7NYRmVCD5vizV98FJ5//PIHbD1gL3a9b2MPAc6k7NV8tl + # ... + # 4SbuJPAiJxC1LQ0t39dR6oMCAMab3hXQqhL56LrR6cRBp6Mtlphv7alu9xb/x51y2x+g2zWtsf80 + # Jrv/vKMsIh/sAyuogb7hqMtp55ecnKxceg== + # -----END CERTIFICATE ----- + cert: '' + + # Client private key + # key: | + # -----BEGIN PRIVATE KEY----- + # MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3DmJtLRmJGY4xU1QtI3yjvxO6 + # bNuyE4z1NF6Xn7VSbcAaQtavWQ6GZi5uukMo+W5DHVtEkgDwh92ySZMuJdJogFbNvJvHAayheCdN + # 7mCQ2UUT9jGXIbmksUn9QMeJVXTZjgJWJzPXToeUdinx9G7+lpVa62UATEd1gaI3oyL72WmpDy/C + # rntnF4d+0dd7zP3jrWkbdtoqjLDT/5D7NYRmVCD5vizV98FJ5//PIHbD1gL3a9b2MPAc6k7NV8tl + # ... + # +9IhSYX+XIg7BZOVDeYqlPfxRvQh8vy3qjt/KUihmEPioAjLaGiihs1Fk5ctLk9A2hIUyP+sEQv9 + # l6RG+a/mW+0rCWn8JAd464Ps9hE= + # -----END PRIVATE KEY----- + key: '' ## ## Set a timeout, in seconds, for LDAP queries. This helps avoid blocking diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb index 7ceb96f502b..dddba85e629 100644 --- a/lib/gitlab/auth/ldap/config.rb +++ b/lib/gitlab/auth/ldap/config.rb @@ -75,7 +75,8 @@ module Gitlab encryption: options['encryption'], filter: omniauth_user_filter, name_proc: name_proc, - disable_verify_certificates: !options['verify_certificates'] + disable_verify_certificates: !options['verify_certificates'], + tls_options: tls_options ) if has_auth? @@ -85,9 +86,6 @@ 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 @@ -196,24 +194,28 @@ module Gitlab end def encryption_options - method = translate_method(options['encryption']) + method = translate_method return nil unless method { method: method, - tls_options: tls_options(method) + tls_options: tls_options } end - def translate_method(method_from_config) - NET_LDAP_ENCRYPTION_METHOD[method_from_config.to_sym] + def translate_method + NET_LDAP_ENCRYPTION_METHOD[options['encryption']&.to_sym] end - def tls_options(method) - return { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless method + def tls_options + return @tls_options if defined?(@tls_options) + + method = translate_method + return nil unless method - opts = if options['verify_certificates'] - OpenSSL::SSL::SSLContext::DEFAULT_PARAMS + opts = if options['verify_certificates'] && method != 'plain' + # Dup so we don't accidentally overwrite the constant + OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.dup 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. @@ -222,10 +224,35 @@ module Gitlab { verify_mode: OpenSSL::SSL::VERIFY_NONE } end - opts[:ca_file] = options['ca_file'] if options['ca_file'].present? - opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present? + opts.merge!(custom_tls_options) - opts + @tls_options = opts + end + + def custom_tls_options + return {} unless options['tls_options'] + + # Dup so we don't overwrite the original value + custom_options = options['tls_options'].dup.delete_if { |_, value| value.nil? || value.blank? } + custom_options.symbolize_keys! + + if custom_options[:cert] + begin + custom_options[:cert] = OpenSSL::X509::Certificate.new(custom_options[:cert]) + rescue OpenSSL::X509::CertificateError => e + Rails.logger.error "LDAP TLS Options 'cert' is invalid for provider #{provider}: #{e.message}" + end + end + + if custom_options[:key] + begin + custom_options[:key] = OpenSSL::PKey.read(custom_options[:key]) + rescue OpenSSL::PKey::PKeyError => e + Rails.logger.error "LDAP TLS Options 'key' is invalid for provider #{provider}: #{e.message}" + end + end + + custom_options end def auth_options diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb index d3ab599d5a0..b91a09e3137 100644 --- a/spec/lib/gitlab/auth/ldap/config_spec.rb +++ b/spec/lib/gitlab/auth/ldap/config_spec.rb @@ -5,6 +5,65 @@ describe Gitlab::Auth::LDAP::Config do let(:config) { described_class.new('ldapmain') } + def raw_cert + <<-EOS +-----BEGIN CERTIFICATE----- +MIIDZjCCAk4CCQDX+u/9fICksDANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJV +UzEMMAoGA1UECAwDRm9vMQwwCgYDVQQHDANCYXIxDDAKBgNVBAoMA0JhejEMMAoG +A1UECwwDUXV4MQ0wCwYDVQQDDARsZGFwMR8wHQYJKoZIhvcNAQkBFhBsZGFwQGV4 +YW1wbGUuY29tMB4XDTE5MDIyNzE1NTUxNFoXDTE5MDMyOTE1NTUxNFowdTELMAkG +A1UEBhMCVVMxDDAKBgNVBAgMA0ZvbzEMMAoGA1UEBwwDQmFyMQwwCgYDVQQKDANC +YXoxDDAKBgNVBAsMA1F1eDENMAsGA1UEAwwEbGRhcDEfMB0GCSqGSIb3DQEJARYQ +bGRhcEBleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +APuDB/4/AUmTEmhYzN13no4Kt8hkRbLQuENRHlOeQw05/MVdoB1AWLOPzIXn4kex +GD9tHkoJl8S0QPmAAcPHn5O97e+gd0ze5dRQZl/cSd2/j5zeaMvZ1mCrPN/dOluM +94Oj+wQU4bEcOlrqIMSh0ezJw10R3IHXCQFeGtIZU57WmKcrryQX4kP7KTOgRw/t +CYp+NivQHtLbBEj1MU0l10qMS2+w8Qpqov4MdW4gx4wTgId2j1ZZ56+n6Jsc9qoI +wBWBNL4XU5a3kwhYZDOJoOvI9po33KLdT1dXS81uOFXClp3LGmKDgLTwQ1w+RmQG ++JG4EvTfDIShdcTDXEaOfCECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJM9Btu5g +k8qDiz5TilvpyoGuI4viCwusARFAFmOB/my/cHlVvkuq4bbfV1KJoWWGJg8GcklL +cnIdxc35uYM5icr6xXQyrW0GqAO+LEXyUxVQqYETxrQ/LJ03xhBnuF7hvZJIBiky +GwUy0clJxGfaCeEM8zXwePawLgGjuUawDDQOwigysoWqoMu3VFW8zl8UPa84bow9 +Kn2QmPAkLw4EcqYSCNSSvnyzu5SM64jwLWRXFsmlqD7773oT29vTkqM1EQANFEfT +7gQomLyPqoPBoFph5oSNn6Rf31QX1Sie92EAKVnZ1XmD68hKzjv6ChCtzTv4jABg +XrDwnLkORIAF/Q== +-----END CERTIFICATE----- + EOS + end + + def raw_key + <<-EOS +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD7gwf+PwFJkxJo +WMzdd56OCrfIZEWy0LhDUR5TnkMNOfzFXaAdQFizj8yF5+JHsRg/bR5KCZfEtED5 +gAHDx5+Tve3voHdM3uXUUGZf3Endv4+c3mjL2dZgqzzf3TpbjPeDo/sEFOGxHDpa +6iDEodHsycNdEdyB1wkBXhrSGVOe1pinK68kF+JD+ykzoEcP7QmKfjYr0B7S2wRI +9TFNJddKjEtvsPEKaqL+DHVuIMeME4CHdo9WWeevp+ibHPaqCMAVgTS+F1OWt5MI +WGQziaDryPaaN9yi3U9XV0vNbjhVwpadyxpig4C08ENcPkZkBviRuBL03wyEoXXE +w1xGjnwhAgMBAAECggEAbw82GVui6uUpjLAhjm3CssAi1TcJ2+L0aq1IMe5Bd3ay +mkg0apY+VNPboQl6zuNxbJh3doPz42UhB8sxfE0Ktwd4KIb4Bxap7+2stwmkCGoN +NVy0c8d2NWuHzuZ2XXTK2vMu5Wd/HWD0l66o14sJEoEpZlB7yU216UevmjSayxjh +aBTSaYyyrf24haTaCuqwph/V73ZlMpFdSALGny0uiP/5inxciMCkMpHfX6BflSb4 +EGKsIYt9BJ0kY4GNG5bCP7971UCxp2eEJhU2fV8HuFGCOD12IqSpUqPxHxjsWpfx +T7FZ3V2kM/58Ca+5LB2y3atcPIdY0/g7/43V4VD+7QKBgQD/PO4/0cmZuuLU1LPT +C/C596kPK0JLlvvRqhbz4byRAkW/n7uQFG7TMtFNle3UmT7rk7pjtbHnByqzEd+9 +jMhBysjHOMg0+DWm7fEtSg/tJ3qLVO3nbdA4qmXYobLcLoG+PCYRLskEHHqTG/Bv +QZLbavOU6rrTqckNr1TMpNBmXwKBgQD8Q0C2YTOpwgjRUe8i6Chnc3o4x8a1i98y +9la6c7y7acWHSbEczMkNfEBrbM73rTb+bBA0Zqw+Z1gkv8bGpvGxX8kbSfJJ2YKW +9koxpLNTVNVapqBa9ImiaozV285dz9Ukx8bnMOJlTELpOl7RRV7iF0smYjfHIl3D +Yxyda/MtfwKBgHb9l/Dmw77IkqE4PFFimqqIHCe3OiP1UpavXh36midcUNoCBLYp +4HTTlyI9iG/5tYysBVQgy7xx6eUrqww6Ss3pVOsTvLp9EL4u5aYAhiZApm+4e2TO +HCmevvZcg/8EK3Zdoj2Wex5QjJBykQe9IVLrrH07ZTfySon3uGfjWkivAoGAGvqS +VC8HGHOw/7n0ilYr5Ax8mM/813OzFj80PVKdb6m7P2HJOFxKcE/Gj/aeF+0FgaZL +AV+tsirZSWzdNGesV5z35Bw/dlh11/FVNAP6TcI34y8I3VFj2uPsVf7hDjVpBTr8 +ccNPoyfJzCm69ESoBiQZnGxKrNhnELtr1wYxhr8CgYApWwf4hVrTWV1zs+pEJenh +AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK +0Ff8afd2Q/OfBeUdq9KA4JO9fNqzEwOWvv8Ryn4ZSYcAuLP7IVJKjjI6R7rYaO/G +3OWJdizbykGOi0BFDu+3dw== +-----END PRIVATE KEY----- + EOS + end + describe '.servers' do it 'returns empty array if no server information is available' do allow(Gitlab.config).to receive(:ldap).and_return('enabled' => false) @@ -89,6 +148,42 @@ describe Gitlab::Auth::LDAP::Config do expect(config.adapter_options[:encryption]).to include({ method: :start_tls }) end + it 'transforms SSL cert and key to OpenSSL objects' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => raw_cert, + 'key' => raw_key + } + } + ) + + expect(config.adapter_options[:encryption][:tls_options][:cert]).to be_a(OpenSSL::X509::Certificate) + expect(config.adapter_options[:encryption][:tls_options][:key]).to be_a(OpenSSL::PKey::RSA) + end + + it 'logs an error when an invalid key or cert are configured' do + allow(Rails.logger).to receive(:error) + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => 'invalid cert', + 'key' => 'invalid_key' + } + } + ) + + config.adapter_options + + expect(Rails.logger).to have_received(:error).with(/LDAP TLS Options/).twice + end + context 'when verify_certificates is enabled' do it 'sets tls_options to OpenSSL defaults' do stub_ldap_config( @@ -130,7 +225,9 @@ describe Gitlab::Auth::LDAP::Config do 'host' => 'ldap.example.com', 'port' => 686, 'encryption' => 'simple_tls', - 'ca_file' => '/etc/ca.pem' + 'tls_options' => { + 'ca_file' => '/etc/ca.pem' + } } ) @@ -145,7 +242,9 @@ describe Gitlab::Auth::LDAP::Config do 'host' => 'ldap.example.com', 'port' => 686, 'encryption' => 'simple_tls', - 'ca_file' => ' ' + 'tls_options' => { + 'ca_file' => ' ' + } } ) @@ -160,7 +259,9 @@ describe Gitlab::Auth::LDAP::Config do 'host' => 'ldap.example.com', 'port' => 686, 'encryption' => 'simple_tls', - 'ssl_version' => 'TLSv1_2' + 'tls_options' => { + 'ssl_version' => 'TLSv1_2' + } } ) @@ -175,7 +276,9 @@ describe Gitlab::Auth::LDAP::Config do 'host' => 'ldap.example.com', 'port' => 686, 'encryption' => 'simple_tls', - 'ssl_version' => ' ' + 'tls_options' => { + 'ssl_version' => ' ' + } } ) @@ -223,6 +326,23 @@ describe Gitlab::Auth::LDAP::Config do ) end + it 'transforms SSL cert and key to OpenSSL objects' do + stub_ldap_config( + options: { + 'host' => 'ldap.example.com', + 'port' => 686, + 'encryption' => 'start_tls', + 'tls_options' => { + 'cert' => raw_cert, + 'key' => raw_key + } + } + ) + + expect(config.omniauth_options[:tls_options][:cert]).to be_a(OpenSSL::X509::Certificate) + expect(config.omniauth_options[:tls_options][:key]).to be_a(OpenSSL::PKey::RSA) + end + context 'when verify_certificates is enabled' do it 'specifies disable_verify_certificates as false' do stub_ldap_config( @@ -261,11 +381,13 @@ describe Gitlab::Auth::LDAP::Config do 'port' => 686, 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'ca_file' => '/etc/ca.pem' + 'tls_options' => { + 'ca_file' => '/etc/ca.pem' + } } ) - expect(config.omniauth_options).to include({ ca_file: '/etc/ca.pem' }) + expect(config.omniauth_options[:tls_options]).to include({ ca_file: '/etc/ca.pem' }) end end @@ -277,11 +399,13 @@ describe Gitlab::Auth::LDAP::Config do 'port' => 686, 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'ca_file' => ' ' + 'tls_options' => { + 'ca_file' => ' ' + } } ) - expect(config.omniauth_options).not_to have_key(:ca_file) + expect(config.omniauth_options[:tls_options]).not_to have_key(:ca_file) end end @@ -293,11 +417,13 @@ describe Gitlab::Auth::LDAP::Config do 'port' => 686, 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'ssl_version' => 'TLSv1_2' + 'tls_options' => { + 'ssl_version' => 'TLSv1_2' + } } ) - expect(config.omniauth_options).to include({ ssl_version: 'TLSv1_2' }) + expect(config.omniauth_options[:tls_options]).to include({ ssl_version: 'TLSv1_2' }) end end @@ -309,11 +435,14 @@ describe Gitlab::Auth::LDAP::Config do 'port' => 686, 'encryption' => 'simple_tls', 'verify_certificates' => true, - 'ssl_version' => ' ' + 'tls_options' => { + 'ssl_version' => ' ' + } } ) - expect(config.omniauth_options).not_to have_key(:ssl_version) + # OpenSSL default params includes `ssl_version` so we just check that it's not blank + expect(config.omniauth_options[:tls_options]).not_to include({ ssl_version: ' ' }) end end end -- GitLab From a1b0b3e4ebb61e952b6e60e7be05bd96286bc298 Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Fri, 1 Mar 2019 17:33:18 +0100 Subject: [PATCH 54/78] Extended Web IDE API to get MRs for a certain branch in a project New `Api.projectMergeRequests` allows: - to query for all MRs on a project without specifying `mergeRequestId` - to filter the returned MRs using parameters from https://bit.ly/2H7We4V The new API request is used for fetching information about MRs associated with a particular branch in Web IDE to have IdeSidebar behave consistently in both scenarios: - getting to a branch from and MR (where we already have info about relevant MR), or - getting to a branch somehow differently directly For cases where there are several merge requests that the current branch is associated with, mark the most recent one as 'current' Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/49663 --- app/assets/javascripts/api.js | 17 ++++ app/assets/javascripts/ide/services/index.js | 3 + .../ide/stores/actions/merge_request.js | 32 +++++++ .../javascripts/ide/stores/actions/project.js | 29 +++--- .../49663-branch-to-mr-connection.yml | 5 + spec/javascripts/api_spec.js | 34 +++++++ .../ide/stores/actions/merge_request_spec.js | 93 +++++++++++++++++++ .../ide/stores/actions/project_spec.js | 1 + 8 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/49663-branch-to-mr-connection.yml diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 85eb08cc97d..8754c253881 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -12,6 +12,7 @@ const Api = { projectsPath: '/api/:version/projects.json', projectPath: '/api/:version/projects/:id', projectLabelsPath: '/:namespace_path/:project_path/labels', + projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', @@ -111,6 +112,22 @@ const Api = { return axios.get(url); }, + /** + * Get all Merge Requests for a project, eventually filtering based on + * supplied parameters + * @param projectPath + * @param params + * @returns {Promise} + */ + projectMergeRequests(projectPath, params = {}) { + const url = Api.buildUrl(Api.projectMergeRequestsPath).replace( + ':id', + encodeURIComponent(projectPath), + ); + + return axios.get(url, { params }); + }, + // Return Merge Request for project projectMergeRequest(projectPath, mergeRequestId, params = {}) { const url = Api.buildUrl(Api.projectMergeRequestPath) diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 13449592e62..ba33b6826d6 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -40,6 +40,9 @@ export default { getProjectData(namespace, project) { return Api.project(`${namespace}/${project}`); }, + getProjectMergeRequests(projectId, params = {}) { + return Api.projectMergeRequests(projectId, params); + }, getProjectMergeRequestData(projectId, mergeRequestId, params = {}) { return Api.projectMergeRequest(projectId, mergeRequestId, params); }, diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 18c24369996..362ced248a1 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -4,6 +4,38 @@ import service from '../../services'; import * as types from '../mutation_types'; import { activityBarViews } from '../../constants'; +export const getMergeRequestsForBranch = ({ commit }, { projectId, branchId } = {}) => + service + .getProjectMergeRequests(`${projectId}`, { + source_branch: branchId, + order_by: 'created_at', + per_page: 1, + }) + .then(({ data }) => { + if (data.length > 0) { + const currentMR = data[0]; + + commit(types.SET_MERGE_REQUEST, { + projectPath: projectId, + mergeRequestId: currentMR.iid, + mergeRequest: currentMR, + }); + + commit(types.SET_CURRENT_MERGE_REQUEST, `${currentMR.iid}`); + } + }) + .catch(e => { + flash( + __(`Error fetching merge requests for ${branchId}`), + 'alert', + document, + null, + false, + true, + ); + throw e; + }); + export const getMergeRequestData = ( { commit, dispatch, state }, { projectId, mergeRequestId, targetProjectId = null, force = false } = {}, diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index b65f631c99c..06ed5c0b572 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -136,17 +136,24 @@ export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath return dispatch('getFiles', { projectId, branchId, - }).then(() => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(state.entries).find( - key => key === path && !state.entries[key].pending, - ); - const treeEntry = state.entries[treeEntryKey]; + }) + .then(() => { + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; + const treeEntryKey = Object.keys(state.entries).find( + key => key === path && !state.entries[key].pending, + ); + const treeEntry = state.entries[treeEntryKey]; - if (treeEntry) { - dispatch('handleTreeEntryAction', treeEntry); + if (treeEntry) { + dispatch('handleTreeEntryAction', treeEntry); + } } - } - }); + }) + .then(() => { + dispatch('getMergeRequestsForBranch', { + projectId, + branchId, + }); + }); }; diff --git a/changelogs/unreleased/49663-branch-to-mr-connection.yml b/changelogs/unreleased/49663-branch-to-mr-connection.yml new file mode 100644 index 00000000000..d92ed6fd3bf --- /dev/null +++ b/changelogs/unreleased/49663-branch-to-mr-connection.yml @@ -0,0 +1,5 @@ +--- +title: Link to most recent MR from a branch +merge_request: 25689 +author: +type: added diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 1e9470970ff..e537e0e8afc 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -139,6 +139,40 @@ describe('Api', () => { }); }); + describe('projectMergeRequests', () => { + const projectPath = 'abc'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests`; + + it('fetches all merge requests for a project', done => { + const mockData = [{ source_branch: 'foo' }, { source_branch: 'bar' }]; + mock.onGet(expectedUrl).reply(200, mockData); + Api.projectMergeRequests(projectPath) + .then(({ data }) => { + expect(data.length).toEqual(2); + expect(data[0].source_branch).toBe('foo'); + expect(data[1].source_branch).toBe('bar'); + }) + .then(done) + .catch(done.fail); + }); + + it('fetches merge requests filtered with passed params', done => { + const params = { + source_branch: 'bar', + }; + const mockData = [{ source_branch: 'bar' }]; + mock.onGet(expectedUrl, { params }).reply(200, mockData); + + Api.projectMergeRequests(projectPath, params) + .then(({ data }) => { + expect(data.length).toEqual(1); + expect(data[0].source_branch).toBe('bar'); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('projectMergeRequest', () => { it('fetches a merge request', done => { const projectPath = 'abc'; diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js index 9bfc7c397b8..a5839630657 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; import actions, { + getMergeRequestsForBranch, getMergeRequestData, getMergeRequestChanges, getMergeRequestVersions, @@ -27,6 +28,98 @@ describe('IDE store merge request actions', () => { resetStore(store); }); + describe('getMergeRequestsForBranch', () => { + describe('success', () => { + const mrData = { iid: 2, source_branch: 'bar' }; + const mockData = [mrData]; + + describe('base case', () => { + beforeEach(() => { + spyOn(service, 'getProjectMergeRequests').and.callThrough(); + mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, mockData); + }); + + it('calls getProjectMergeRequests service method', done => { + store + .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .then(() => { + expect(service.getProjectMergeRequests).toHaveBeenCalledWith('abcproject', { + source_branch: 'bar', + order_by: 'created_at', + per_page: 1, + }); + + done(); + }) + .catch(done.fail); + }); + + it('sets the "Merge Request" Object', done => { + store + .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .then(() => { + expect(Object.keys(store.state.projects.abcproject.mergeRequests).length).toEqual(1); + expect(Object.keys(store.state.projects.abcproject.mergeRequests)[0]).toEqual('2'); + expect(store.state.projects.abcproject.mergeRequests[2]).toEqual( + jasmine.objectContaining(mrData), + ); + done(); + }) + .catch(done.fail); + }); + + it('sets "Current Merge Request" object to the most recent MR', done => { + store + .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .then(() => { + expect(store.state.currentMergeRequestId).toEqual('2'); + done(); + }) + .catch(done.fail); + }); + }); + + describe('no merge requests for branch available case', () => { + beforeEach(() => { + spyOn(service, 'getProjectMergeRequests').and.callThrough(); + mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, []); + }); + + it('does not fail if there are no merge requests for current branch', done => { + store + .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'foo' }) + .then(() => { + expect(Object.keys(store.state.projects.abcproject.mergeRequests).length).toEqual(0); + expect(store.state.currentMergeRequestId).toEqual(''); + done(); + }) + .catch(done.fail); + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError(); + }); + + it('flashes message, if error', done => { + const flashSpy = spyOnDependency(actions, 'flash'); + + getMergeRequestsForBranch({ commit() {} }, { projectId: 'abcproject', branchId: 'bar' }) + .then(() => { + fail('Expected getMergeRequestsForBranch to throw an error'); + }) + .catch(() => { + expect(flashSpy).toHaveBeenCalled(); + expect(flashSpy.calls.argsFor(0)[0]).toEqual('Error fetching merge requests for bar'); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + describe('getMergeRequestData', () => { describe('success', () => { beforeEach(() => { diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index 7d8c9edd965..7b0963713fb 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -249,6 +249,7 @@ describe('IDE store project actions', () => { ['setCurrentBranchId', branch.branchId], ['getBranchData', branch], ['getFiles', branch], + ['getMergeRequestsForBranch', branch], ]); }) .then(done) -- GitLab From ebf16ada856efb85424a98848c141f21e609886a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Javier=20L=C3=B3pez?= Date: Mon, 4 Mar 2019 18:36:34 +0000 Subject: [PATCH 55/78] Arbitrary file read via MergeRequestDiff --- app/models/merge_request.rb | 2 +- app/models/merge_request_diff.rb | 2 + app/validators/sha_validator.rb | 9 +++++ .../security-fj-diff-import-file-read-fix.yml | 5 +++ .../import_export/merge_request_parser.rb | 11 +++++ .../merge_request/user_sees_versions_spec.rb | 6 ++- .../merge_request_parser_spec.rb | 16 ++++++++ spec/models/merge_request_diff_spec.rb | 14 ++++++- spec/validators/sha_validator_spec.rb | 40 +++++++++++++++++++ ..._pipeline_for_merge_request_worker_spec.rb | 2 +- 10 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 app/validators/sha_validator.rb create mode 100644 changelogs/unreleased/security-fj-diff-import-file-read-fix.yml create mode 100644 spec/validators/sha_validator_spec.rb diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1468ae1c34a..ad4d5be48d4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -71,7 +71,7 @@ class MergeRequest < ActiveRecord::Base serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize - after_create :ensure_merge_request_diff, unless: :importing? + after_create :ensure_merge_request_diff after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed after_save :ensure_metrics diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index e286a4e57f2..351a662ae83 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -22,6 +22,8 @@ class MergeRequestDiff < ActiveRecord::Base has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) } + validates :base_commit_sha, :head_commit_sha, :start_commit_sha, sha: true + state_machine :state, initial: :empty do event :clean do transition any => :without_files diff --git a/app/validators/sha_validator.rb b/app/validators/sha_validator.rb new file mode 100644 index 00000000000..085fca4d65d --- /dev/null +++ b/app/validators/sha_validator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ShaValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if value.blank? || value.match(/\A\h{40}\z/) + + record.errors.add(attribute, 'is not a valid SHA') + end +end diff --git a/changelogs/unreleased/security-fj-diff-import-file-read-fix.yml b/changelogs/unreleased/security-fj-diff-import-file-read-fix.yml new file mode 100644 index 00000000000..e98d4e89712 --- /dev/null +++ b/changelogs/unreleased/security-fj-diff-import-file-read-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fix arbitrary file read via diffs during import +merge_request: +author: +type: security diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb index 040a70d6775..deb2f59f05f 100644 --- a/lib/gitlab/import_export/merge_request_parser.rb +++ b/lib/gitlab/import_export/merge_request_parser.rb @@ -20,6 +20,17 @@ module Gitlab create_target_branch unless branch_exists?(@merge_request.target_branch) end + # The merge_request_diff associated with the current @merge_request might + # be invalid. Than means, when the @merge_request object is saved, the + # @merge_request.merge_request_diff won't. This can leave the merge request + # in an invalid state, because a merge request must have an associated + # merge request diff. + # In this change, if the associated merge request diff is invalid, we set + # it to nil. This change, in association with the after callback + # :ensure_merge_request_diff in the MergeRequest class, makes that + # when the merge request is going to be created and it doesn't have + # one, a default one will be generated. + @merge_request.merge_request_diff = nil unless @merge_request.merge_request_diff&.valid? @merge_request end diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index aa91ade46ca..5c45e363997 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -1,7 +1,11 @@ require 'rails_helper' describe 'Merge request > User sees versions', :js do - let(:merge_request) { create(:merge_request, importing: true) } + let(:merge_request) do + create(:merge_request).tap do |mr| + mr.merge_request_diff.destroy + end + end let(:project) { merge_request.source_project } let(:user) { project.creator } let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } 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 68eaa70e6b6..4b234411a44 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -41,4 +41,20 @@ describe Gitlab::ImportExport::MergeRequestParser do expect(parsed_merge_request).to eq(merge_request) end + + context 'when the merge request has diffs' do + let(:merge_request) do + build(:merge_request, source_project: forked_project, target_project: project) + end + + context 'when the diff is invalid' do + let(:merge_request_diff) { build(:merge_request_diff, merge_request: merge_request, base_commit_sha: 'foobar') } + + it 'sets the diff to nil' do + expect(merge_request_diff).to be_invalid + expect(merge_request_diff.merge_request).to eq merge_request + expect(parsed_merge_request.merge_request_diff).to be_nil + end + end + end end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 1849d3bac12..e530e0302f5 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -3,6 +3,18 @@ require 'spec_helper' describe MergeRequestDiff do let(:diff_with_commits) { create(:merge_request).merge_request_diff } + describe 'validations' do + subject { diff_with_commits } + + it 'checks sha format of base_commit_sha, head_commit_sha and start_commit_sha' do + subject.base_commit_sha = subject.head_commit_sha = subject.start_commit_sha = 'foobar' + + expect(subject.valid?).to be false + expect(subject.errors.count).to eq 3 + expect(subject.errors).to all(include('is not a valid SHA')) + end + end + describe 'create new record' do subject { diff_with_commits } @@ -78,7 +90,7 @@ describe MergeRequestDiff do it 'returns persisted diffs if cannot compare with diff refs' do expect(diff).to receive(:load_diffs).and_call_original - diff.update!(head_commit_sha: 'invalid-sha') + diff.update!(head_commit_sha: Digest::SHA1.hexdigest(SecureRandom.hex)) diff.diffs.diff_files end diff --git a/spec/validators/sha_validator_spec.rb b/spec/validators/sha_validator_spec.rb new file mode 100644 index 00000000000..b9242ef931e --- /dev/null +++ b/spec/validators/sha_validator_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe ShaValidator do + let(:validator) { described_class.new(attributes: [:base_commit_sha]) } + let(:merge_diff) { build(:merge_request_diff) } + + subject { validator.validate_each(merge_diff, :base_commit_sha, value) } + + context 'with empty value' do + let(:value) { nil } + + it 'does not add any error if value is empty' do + subject + + expect(merge_diff.errors).to be_empty + end + end + + context 'with valid sha' do + let(:value) { Digest::SHA1.hexdigest(SecureRandom.hex) } + + it 'does not add any error if value is empty' do + subject + + expect(merge_diff.errors).to be_empty + end + end + + context 'with invalid sha' do + let(:value) { 'foo' } + + it 'adds error to the record' do + expect(merge_diff.errors).to be_empty + + subject + + expect(merge_diff.errors).not_to be_empty + end + end +end diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb index 963237ceadf..f29e49f202a 100644 --- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb +++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb @@ -18,7 +18,7 @@ describe UpdateHeadPipelineForMergeRequestWorker do context 'when merge request sha does not equal pipeline sha' do before do - merge_request.merge_request_diff.update(head_commit_sha: 'different_sha') + merge_request.merge_request_diff.update(head_commit_sha: Digest::SHA1.hexdigest(SecureRandom.hex)) end it 'does not update head pipeline' do -- GitLab From 50591efc75e465c1b626c1bb2d4d65447ee9a726 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Mon, 4 Mar 2019 18:36:43 +0000 Subject: [PATCH 56/78] Check validity of prometheus_service before query Check validity before querying so that if the dns entry for the api_url has been changed to something invalid after the model was saved and checked for validity, it will not query. This is to solve a toctou (time of check to time of use) issue. --- .../project_services/prometheus_service.rb | 6 +- ...y-55468-check-validity-before-querying.yml | 5 ++ .../prometheus_service_spec.rb | 61 +++++++++++++------ 3 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 changelogs/unreleased/security-55468-check-validity-before-querying.yml diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 60cb2d380d5..c68a9d923c8 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -71,7 +71,7 @@ class PrometheusService < MonitoringService end def prometheus_client - RestClient::Resource.new(api_url, max_redirects: 0) if api_url && manual_configuration? && active? + RestClient::Resource.new(api_url, max_redirects: 0) if should_return_client? end def prometheus_available? @@ -83,6 +83,10 @@ class PrometheusService < MonitoringService private + def should_return_client? + api_url && manual_configuration? && active? && valid? + end + def synchronize_service_state self.active = prometheus_available? || manual_configuration? diff --git a/changelogs/unreleased/security-55468-check-validity-before-querying.yml b/changelogs/unreleased/security-55468-check-validity-before-querying.yml new file mode 100644 index 00000000000..8bb11a97f52 --- /dev/null +++ b/changelogs/unreleased/security-55468-check-validity-before-querying.yml @@ -0,0 +1,5 @@ +--- +title: Fix blind SSRF in Prometheus integration by checking URL before querying +merge_request: +author: +type: security diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index b6cf4c72450..e9c7c94ad70 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -33,18 +33,38 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do describe 'Validations' do context 'when manual_configuration is enabled' do before do - subject.manual_configuration = true + service.manual_configuration = true end - it { is_expected.to validate_presence_of(:api_url) } + it 'validates presence of api_url' do + expect(service).to validate_presence_of(:api_url) + end end context 'when manual configuration is disabled' do before do - subject.manual_configuration = false + service.manual_configuration = false end - it { is_expected.not_to validate_presence_of(:api_url) } + it 'does not validate presence of api_url' do + expect(service).not_to validate_presence_of(:api_url) + end + end + + context 'when the api_url domain points to localhost or local network' do + let(:domain) { Addressable::URI.parse(service.api_url).hostname } + + it 'cannot query' do + expect(service.can_query?).to be true + + aggregate_failures do + ['127.0.0.1', '192.168.2.3'].each do |url| + allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)]) + + expect(service.can_query?).to be false + end + end + end end end @@ -74,30 +94,35 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do end describe '#prometheus_client' do + let(:api_url) { 'http://some_url' } + + before do + service.active = true + service.api_url = api_url + service.manual_configuration = manual_configuration + end + context 'manual configuration is enabled' do - let(:api_url) { 'http://some_url' } + let(:manual_configuration) { true } - before do - subject.active = true - subject.manual_configuration = true - subject.api_url = api_url + it 'returns rest client from api_url' do + expect(service.prometheus_client.url).to eq(api_url) end - it 'returns rest client from api_url' do - expect(subject.prometheus_client.url).to eq(api_url) + it 'calls valid?' do + allow(service).to receive(:valid?).and_call_original + + expect(service.prometheus_client).not_to be_nil + + expect(service).to have_received(:valid?) end end context 'manual configuration is disabled' do - let(:api_url) { 'http://some_url' } - - before do - subject.manual_configuration = false - subject.api_url = api_url - end + let(:manual_configuration) { false } it 'no client provided' do - expect(subject.prometheus_client).to be_nil + expect(service.prometheus_client).to be_nil end end end -- GitLab From 5169dafccf70bf50d780bfdbeea0802f094520cc Mon Sep 17 00:00:00 2001 From: Igor Drozdov Date: Mon, 4 Mar 2019 18:37:08 +0000 Subject: [PATCH 57/78] Forbid creating discussions for users with restricted access --- ...y-id-restricted-access-to-private-repo.yml | 5 + lib/api/helpers/notes_helpers.rb | 14 +-- spec/policies/commit_policy_spec.rb | 53 +++++++++++ spec/policies/note_policy_spec.rb | 94 +++++++++---------- .../requests/api/discussions.rb | 31 ++++++ 5 files changed, 137 insertions(+), 60 deletions(-) create mode 100644 changelogs/unreleased/security-id-restricted-access-to-private-repo.yml create mode 100644 spec/policies/commit_policy_spec.rb diff --git a/changelogs/unreleased/security-id-restricted-access-to-private-repo.yml b/changelogs/unreleased/security-id-restricted-access-to-private-repo.yml new file mode 100644 index 00000000000..7d7478d297b --- /dev/null +++ b/changelogs/unreleased/security-id-restricted-access-to-private-repo.yml @@ -0,0 +1,5 @@ +--- +title: Forbid creating discussions for users with restricted access +merge_request: +author: +type: security diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 216b2c45741..795dca5cf03 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -70,14 +70,7 @@ module API def find_noteable(parent, noteables_str, noteable_id) noteable = public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend - readable = - if noteable.is_a?(Commit) - # for commits there is not :read_commit policy, check if user - # has :read_note permission on the commit's project - can?(current_user, :read_note, user_project) - else - can?(current_user, noteable_read_ability_name(noteable), noteable) - end + readable = can?(current_user, noteable_read_ability_name(noteable), noteable) return not_found!(noteables_str) unless readable @@ -89,12 +82,11 @@ module API end def create_note(noteable, opts) - policy_object = noteable.is_a?(Commit) ? user_project : noteable - authorize!(:create_note, policy_object) + authorize!(:create_note, noteable) parent = noteable_parent(noteable) - opts.delete(:created_at) unless current_user.can?(:set_note_created_at, policy_object) + opts.delete(:created_at) unless current_user.can?(:set_note_created_at, noteable) opts[:updated_at] = opts[:created_at] if opts[:created_at] diff --git a/spec/policies/commit_policy_spec.rb b/spec/policies/commit_policy_spec.rb new file mode 100644 index 00000000000..2259693cf01 --- /dev/null +++ b/spec/policies/commit_policy_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe CommitPolicy do + describe '#rules' do + let(:user) { create(:user) } + let(:commit) { project.repository.head_commit } + let(:policy) { described_class.new(user, commit) } + + context 'when project is public' do + let(:project) { create(:project, :public, :repository) } + + it 'can read commit and create a note' do + expect(policy).to be_allowed(:read_commit) + end + + context 'when repository access level is private' do + let(:project) { create(:project, :public, :repository, :repository_private) } + + it 'can not read commit and create a note' do + expect(policy).to be_disallowed(:read_commit) + end + + context 'when the user is a project member' do + before do + project.add_developer(user) + end + + it 'can read commit and create a note' do + expect(policy).to be_allowed(:read_commit) + end + end + end + end + + context 'when project is private' do + let(:project) { create(:project, :private, :repository) } + + it 'can not read commit and create a note' do + expect(policy).to be_disallowed(:read_commit) + end + + context 'when the user is a project member' do + before do + project.add_developer(user) + end + + it 'can read commit and create a note' do + expect(policy).to be_allowed(:read_commit) + end + end + end + end +end diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb index 0e848c74659..4be7a0266d1 100644 --- a/spec/policies/note_policy_spec.rb +++ b/spec/policies/note_policy_spec.rb @@ -1,28 +1,15 @@ require 'spec_helper' -describe NotePolicy, mdoels: true do +describe NotePolicy do describe '#rules' do let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } - - def policies(noteable = nil) - return @policies if @policies - - noteable ||= issue - note = if noteable.is_a?(Commit) - create(:note_on_commit, commit_id: noteable.id, author: user, project: project) - else - create(:note, noteable: noteable, author: user, project: project) - end - - @policies = described_class.new(user, note) - end + let(:noteable) { issue } + let(:policy) { described_class.new(user, note) } + let(:note) { create(:note, noteable: noteable, author: user, project: project) } shared_examples_for 'a discussion with a private noteable' do - let(:noteable) { issue } - let(:policy) { policies(noteable) } - context 'when the note author can no longer see the noteable' do it 'can not edit nor read the note' do expect(policy).to be_disallowed(:admin_note) @@ -46,12 +33,21 @@ describe NotePolicy, mdoels: true do end end - context 'when the project is private' do - let(:project) { create(:project, :private, :repository) } + context 'when the noteable is a commit' do + let(:commit) { project.repository.head_commit } + let(:note) { create(:note_on_commit, commit_id: commit.id, author: user, project: project) } + + context 'when the project is private' do + let(:project) { create(:project, :private, :repository) } + + it_behaves_like 'a discussion with a private noteable' + end - context 'when the noteable is a commit' do - it_behaves_like 'a discussion with a private noteable' do - let(:noteable) { project.repository.head_commit } + context 'when the project is public' do + context 'when repository access level is private' do + let(:project) { create(:project, :public, :repository, :repository_private) } + + it_behaves_like 'a discussion with a private noteable' end end end @@ -59,44 +55,44 @@ describe NotePolicy, mdoels: true do context 'when the project is public' do context 'when the note author is not a project member' do it 'can edit a note' do - expect(policies).to be_allowed(:admin_note) - expect(policies).to be_allowed(:resolve_note) - expect(policies).to be_allowed(:read_note) + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) end end context 'when the noteable is a project snippet' do - it 'can edit note' do - policies = policies(create(:project_snippet, :public, project: project)) + let(:noteable) { create(:project_snippet, :public, project: project) } - expect(policies).to be_allowed(:admin_note) - expect(policies).to be_allowed(:resolve_note) - expect(policies).to be_allowed(:read_note) + it 'can edit note' do + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) end context 'when it is private' do - it_behaves_like 'a discussion with a private noteable' do - let(:noteable) { create(:project_snippet, :private, project: project) } - end + let(:noteable) { create(:project_snippet, :private, project: project) } + + it_behaves_like 'a discussion with a private noteable' end end context 'when the noteable is a personal snippet' do - it 'can edit note' do - policies = policies(create(:personal_snippet, :public)) + let(:noteable) { create(:personal_snippet, :public) } - expect(policies).to be_allowed(:admin_note) - expect(policies).to be_allowed(:resolve_note) - expect(policies).to be_allowed(:read_note) + it 'can edit note' do + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) end context 'when it is private' do - it 'can not edit nor read the note' do - policies = policies(create(:personal_snippet, :private)) + let(:noteable) { create(:personal_snippet, :private) } - expect(policies).to be_disallowed(:admin_note) - expect(policies).to be_disallowed(:resolve_note) - expect(policies).to be_disallowed(:read_note) + it 'can not edit nor read the note' do + expect(policy).to be_disallowed(:admin_note) + expect(policy).to be_disallowed(:resolve_note) + expect(policy).to be_disallowed(:read_note) end end end @@ -120,20 +116,20 @@ describe NotePolicy, mdoels: true do end it 'can edit a note' do - expect(policies).to be_allowed(:admin_note) - expect(policies).to be_allowed(:resolve_note) - expect(policies).to be_allowed(:read_note) + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) end end context 'when the note author is not a project member' do it 'can not edit a note' do - expect(policies).to be_disallowed(:admin_note) - expect(policies).to be_disallowed(:resolve_note) + expect(policy).to be_disallowed(:admin_note) + expect(policy).to be_disallowed(:resolve_note) end it 'can read a note' do - expect(policies).to be_allowed(:read_note) + expect(policy).to be_allowed(:read_note) end end end diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb index e44da4faa5a..eff8e401bad 100644 --- a/spec/support/shared_examples/requests/api/discussions.rb +++ b/spec/support/shared_examples/requests/api/discussions.rb @@ -86,6 +86,37 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name| expect(response).to have_gitlab_http_status(404) end end + + context 'when a project is public with private repo access' do + let!(:parent) { create(:project, :public, :repository, :repository_private, :snippets_private) } + let!(:user_without_access) { create(:user) } + + context 'when user is not a team member of private repo' do + before do + project.team.truncate + end + + context "creating a new note" do + before do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user_without_access), params: { body: 'hi!' } + end + + it 'raises 404 error' do + expect(response).to have_gitlab_http_status(404) + end + end + + context "fetching a discussion" do + before do + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{note.discussion_id}", user_without_access) + end + + it 'raises 404 error' do + expect(response).to have_gitlab_http_status(404) + end + end + end + end end describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes" do -- GitLab From 17ff3eaa2ac5d0d6e9fd39eefa778d4a793d1029 Mon Sep 17 00:00:00 2001 From: Gokhan Apaydin Date: Mon, 4 Mar 2019 20:28:37 +0000 Subject: [PATCH 58/78] Fix commit page info well from overflowing container in side-by-side mode --- app/assets/stylesheets/pages/issuable.scss | 1 + changelogs/unreleased/gokhanap-master-patch-03762.yml | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 changelogs/unreleased/gokhanap-master-patch-03762.yml diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 3796ef47c32..623fa485ba6 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -22,6 +22,7 @@ .detail-page-header, .page-content-header, .commit-box, + .info-well, .commit-ci-menu, .files-changed-inner, .limited-header-width, diff --git a/changelogs/unreleased/gokhanap-master-patch-03762.yml b/changelogs/unreleased/gokhanap-master-patch-03762.yml new file mode 100644 index 00000000000..22ab453e359 --- /dev/null +++ b/changelogs/unreleased/gokhanap-master-patch-03762.yml @@ -0,0 +1,5 @@ +--- +title: 'commit page info-well overflow fix #56436' +merge_request: 24799 +author: Gokhan Apaydin +type: fixed -- GitLab From 2b0a0f9b03d8411c0b6e312e9399fe74cfa5dfea Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Mon, 4 Mar 2019 21:07:36 +0000 Subject: [PATCH 59/78] Merge branch 'master' into expose-additional-merge-request-pipeline-variables Conflicts: doc/ci/variables/README.md spec/models/ci/pipeline_spec.rb --- app/models/merge_request.rb | 50 ++++++++----------- ...ional-merge-request-pipeline-variables.yml | 5 ++ doc/ci/variables/README.md | 30 ++++++----- spec/models/ci/pipeline_spec.rb | 39 ++++++++++++++- 4 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 changelogs/unreleased/expose-additional-merge-request-pipeline-variables.yml diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1468ae1c34a..1e4735bac8c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1154,35 +1154,16 @@ class MergeRequest < ActiveRecord::Base Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s) variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s) - - variables.append(key: 'CI_MERGE_REQUEST_REF_PATH', - value: ref_path.to_s) - - variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID', - value: project.id.to_s) - - variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', - value: project.full_path) - - variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', - value: project.web_url) - - variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', - value: target_branch.to_s) - - if source_project - variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID', - value: source_project.id.to_s) - - variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', - value: source_project.full_path) - - variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', - value: source_project.web_url) - - variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', - value: source_branch.to_s) - end + variables.append(key: 'CI_MERGE_REQUEST_REF_PATH', value: ref_path.to_s) + variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID', value: project.id.to_s) + variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', value: project.full_path) + variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url) + variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s) + variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title) + variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee + variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone + variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present? + variables.concat(source_project_variables) end end @@ -1389,4 +1370,15 @@ class MergeRequest < ActiveRecord::Base source_project&.ci_pipelines &.latest_for_merge_request(self, source_branch, diff_head_sha) end + + def source_project_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + break variables unless source_project + + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID', value: source_project.id.to_s) + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', value: source_project.full_path) + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', value: source_project.web_url) + variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s) + end + end end diff --git a/changelogs/unreleased/expose-additional-merge-request-pipeline-variables.yml b/changelogs/unreleased/expose-additional-merge-request-pipeline-variables.yml new file mode 100644 index 00000000000..399f60ef219 --- /dev/null +++ b/changelogs/unreleased/expose-additional-merge-request-pipeline-variables.yml @@ -0,0 +1,5 @@ +--- +title: Expose additional merge request pipeline variables +merge_request: 24595 +author: Hiroyuki Sato +type: added diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 6fe352df48a..08db89124de 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -79,19 +79,23 @@ future GitLab releases.** | **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | | **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry][registry] and downloading [dependent repositories][dependent-repositories] | | **CI_JOB_URL** | 11.1 | 0.5 | Job details URL | -| **CI_MERGE_REQUEST_ID** | 11.6 | all | The ID of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_IID** | 11.6 | all | The IID of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_PROJECT_ID** | 11.6 | all | The ID of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_PROJECT_PATH** | 11.6 | all | The path of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) (e.g. `namespace/awesome-project`) | -| **CI_MERGE_REQUEST_PROJECT_URL** | 11.6 | all | The URL of the project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) (e.g. `http://192.168.10.15:3000/namespace/awesome-project`) | -| **CI_MERGE_REQUEST_REF_PATH** | 11.6 | all | The ref path of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md). (e.g. `refs/merge-requests/1/head`) | -| **CI_MERGE_REQUEST_SOURCE_BRANCH_NAME** | 11.6 | all | The source branch name of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_SOURCE_BRANCH_SHA** | 11.9 | all | The HEAD sha of the source branch of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_SOURCE_PROJECT_ID** | 11.6 | all | The ID of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_SOURCE_PROJECT_PATH** | 11.6 | all | The path of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_SOURCE_PROJECT_URL** | 11.6 | all | The URL of the source project of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_TARGET_BRANCH_NAME** | 11.6 | all | The target branch name of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_TARGET_BRANCH_SHA** | 11.9 | all | The HEAD sha of the target branch of the merge request if it's [pipelines for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_ID** | 11.6 | all | The ID of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_IID** | 11.6 | all | The IID of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_PROJECT_ID** | 11.6 | all | The ID of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_PROJECT_PATH** | 11.6 | all | The path of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) (e.g. `namespace/awesome-project`) | +| **CI_MERGE_REQUEST_PROJECT_URL** | 11.6 | all | The URL of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) (e.g. `http://192.168.10.15:3000/namespace/awesome-project`) | +| **CI_MERGE_REQUEST_REF_PATH** | 11.6 | all | The ref path of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). (e.g. `refs/merge-requests/1/head`) | +| **CI_MERGE_REQUEST_SOURCE_BRANCH_NAME** | 11.6 | all | The source branch name of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_SOURCE_BRANCH_SHA** | 11.9 | all | The HEAD sha of the source branch of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_SOURCE_PROJECT_ID** | 11.6 | all | The ID of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_SOURCE_PROJECT_PATH** | 11.6 | all | The path of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_SOURCE_PROJECT_URL** | 11.6 | all | The URL of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_TARGET_BRANCH_NAME** | 11.6 | all | The target branch name of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_TARGET_BRANCH_SHA** | 11.9 | all | The HEAD sha of the target branch of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_TITLE** | 11.9 | all | The title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_ASSIGNEES** | 11.9 | all | Comma-separated usernames of the assignees of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). **Comming soon**: [Multitle assignees for merge requests](https://gitlab.com/gitlab-org/gitlab-ee/issues/2004) | +| **CI_MERGE_REQUEST_MILESTONE** | 11.9 | all | The milestone title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | +| **CI_MERGE_REQUEST_LABELS** | 11.9 | all | Comma-separated label names of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | | **CI_NODE_INDEX** | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. | | **CI_NODE_TOTAL** | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. | | **CI_API_V4_URL** | 11.7 | all | The GitLab API v4 root URL | diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 462793b259b..3a6de57bd63 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -513,9 +513,16 @@ describe Ci::Pipeline, :mailer do source_project: project, source_branch: 'feature', target_project: project, - target_branch: 'master') + target_branch: 'master', + assignee: assignee, + milestone: milestone, + labels: labels) end + let(:assignee) { create(:user) } + let(:milestone) { create(:milestone) } + let(:labels) { create_list(:label, 2) } + it 'exposes merge request pipeline variables' do expect(subject.to_hash) .to include( @@ -531,7 +538,11 @@ describe Ci::Pipeline, :mailer do 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, - 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s) + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s, + 'CI_MERGE_REQUEST_TITLE' => merge_request.title, + 'CI_MERGE_REQUEST_ASSIGNEES' => assignee.username, + 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, + 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(',')) end context 'when source project does not exist' do @@ -547,6 +558,30 @@ describe Ci::Pipeline, :mailer do CI_MERGE_REQUEST_SOURCE_BRANCH_NAME]) end end + + context 'without assignee' do + let(:assignee) { nil } + + it 'does not expose assignee variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES') + end + end + + context 'without milestone' do + let(:milestone) { nil } + + it 'does not expose milestone variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE') + end + end + + context 'without labels' do + let(:labels) { [] } + + it 'does not expose labels variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS') + end + end end end -- GitLab From 18889696bda82d6ec50f97ca06edc97ff49d8fb4 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Fri, 1 Mar 2019 14:10:07 +0900 Subject: [PATCH 60/78] Rename ambiguous association names in Ci::Pipeline fix it ok Fix head pipeline --- app/mailers/emails/pipelines.rb | 2 +- app/models/ci/pipeline.rb | 2 +- app/workers/pipeline_metrics_worker.rb | 2 +- spec/lib/gitlab/import_export/all_models.yml | 2 +- spec/models/ci/pipeline_spec.rb | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 31e183640ad..fb57c0da34d 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -15,7 +15,7 @@ module Emails def pipeline_mail(pipeline, recipients, status) @project = pipeline.project @pipeline = pipeline - @merge_request = pipeline.merge_requests.first + @merge_request = pipeline.merge_requests_as_head_pipeline.first add_headers # We use bcc here because we don't want to generate this emails for a diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 317f965da1c..ca9725f7a04 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -39,7 +39,7 @@ module Ci # Merge requests for which the current pipeline is running against # the merge request's latest commit. - has_many :merge_requests, foreign_key: "head_pipeline_id" + has_many :merge_requests_as_head_pipeline, foreign_key: "head_pipeline_id", class_name: 'MergeRequest' has_many :pending_builds, -> { pending }, 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' diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb index c2fbfd2b3a5..0ddad43b8d5 100644 --- a/app/workers/pipeline_metrics_worker.rb +++ b/app/workers/pipeline_metrics_worker.rb @@ -30,6 +30,6 @@ class PipelineMetricsWorker # rubocop: enable CodeReuse/ActiveRecord def merge_requests(pipeline) - pipeline.merge_requests.map(&:id) + pipeline.merge_requests_as_head_pipeline.map(&:id) end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 018a5d3dd3d..01da3ea7081 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -127,7 +127,7 @@ ci_pipelines: - scheduled_actions - artifacts - pipeline_schedule -- merge_requests +- merge_requests_as_head_pipeline - merge_request - deployments - environments diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3a6de57bd63..332278ce077 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2252,7 +2252,7 @@ describe Ci::Pipeline, :mailer do end end - describe "#merge_requests" do + describe "#merge_requests_as_head_pipeline" do let(:project) { create(:project) } let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') } @@ -2260,20 +2260,20 @@ describe Ci::Pipeline, :mailer do allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { 'a288a022a53a5a944fae87bcec6efc87b7061808' } merge_request = create(:merge_request, source_project: project, head_pipeline: pipeline, source_branch: pipeline.ref) - expect(pipeline.merge_requests).to eq([merge_request]) + expect(pipeline.merge_requests_as_head_pipeline).to eq([merge_request]) end it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') - expect(pipeline.merge_requests).to be_empty + expect(pipeline.merge_requests_as_head_pipeline).to be_empty end it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do create(:merge_request, source_project: project, source_branch: pipeline.ref) allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' } - expect(pipeline.merge_requests).to be_empty + expect(pipeline.merge_requests_as_head_pipeline).to be_empty end end -- GitLab From 999a20dce3a1c3c8e1946a73bfc47f46a6fd2bbd Mon Sep 17 00:00:00 2001 From: Moritz Bellach Date: Tue, 5 Mar 2019 06:11:28 +0000 Subject: [PATCH 61/78] docs: ldap users must have email address set --- doc/administration/auth/ldap.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index d5d0d99ac24..f8d85d68b67 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -81,6 +81,9 @@ library. `tls` corresponds to StartTLS, not to be confused with regular TLS. Normally, if you specify `ssl` it will be on port 636, while `tls` (StartTLS) would be on port 389. `plain` also operates on port 389. +NOTE: **Note:** +LDAP users must have an email address set, regardless of whether it is used to log in. + **Omnibus configuration** ```ruby -- GitLab From 0fc51747b78c7cb3b590f29354fc236bfcb847f4 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 4 Mar 2019 22:27:21 -0800 Subject: [PATCH 62/78] Resolve conflicts in app/policies/group_policy.rb --- app/policies/group_policy.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 2b0f1d25534..298769c0eb8 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -53,11 +53,8 @@ class GroupPolicy < BasePolicy rule { admin }.enable :read_group rule { has_projects }.policy do -<<<<<<< HEAD enable :read_group enable :read_list -======= ->>>>>>> dev/master enable :read_label end -- GitLab From 997290fcba1058650390645d099a7245510470b6 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 4 Mar 2019 22:33:37 -0800 Subject: [PATCH 63/78] Resolve conflicts in spec/mailers/notify_spec.rb --- spec/mailers/notify_spec.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 67af2c36669..3c8897ed37c 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -208,15 +208,6 @@ describe Notify do let(:new_issue) { create(:issue) } subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) } -<<<<<<< HEAD - it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do - let(:model) { issue } - end - it_behaves_like 'it should show Gmail Actions View Issue link' - it_behaves_like 'an unsubscribeable thread' - it_behaves_like 'appearance header and footer enabled' - it_behaves_like 'appearance header and footer not enabled' -======= context 'when a user has permissions to access the new issue' do before do new_issue.project.add_developer(recipient) @@ -234,7 +225,6 @@ describe Notify do it 'has the correct subject and body' do new_issue_url = project_issue_path(new_issue.project, new_issue) ->>>>>>> dev/master aggregate_failures do is_expected.to have_referable_subject(issue, reply: true) -- GitLab From b9fcefc895fe140018b97912891121f76afe6648 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 4 Mar 2019 22:38:02 -0800 Subject: [PATCH 64/78] Resolve conflicts in spec/policies/group_policy_spec.rb --- spec/policies/group_policy_spec.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 5cb0183df3a..4c31ff30fc6 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -114,25 +114,13 @@ describe GroupPolicy do project.add_developer(current_user) end -<<<<<<< HEAD - it do - expect_allowed(:read_group, :read_list, :read_label) - end -======= it { expect_allowed(:read_label) } ->>>>>>> dev/master context 'in subgroups', :nested_groups do let(:subgroup) { create(:group, :private, parent: group) } let(:project) { create(:project, namespace: subgroup) } -<<<<<<< HEAD - it do - expect_allowed(:read_group, :read_list, :read_label) - end -======= it { expect_allowed(:read_label) } ->>>>>>> dev/master end end -- GitLab From 211ae9d093de08c1cf97768121a18add814e2879 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 5 Mar 2019 07:12:13 +0000 Subject: [PATCH 65/78] Add gitlab ui visual test --- .gitlab-ci.yml | 18 ++++++++++++++++++ package.json | 2 +- yarn.lock | 30 ++++++++++++++++++++---------- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2329c9c6edc..fa47aea310d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -672,6 +672,24 @@ gitlab:assets:compile: - docker - gitlab-org +gitlab:ui:visual: + tags: + - gitlab-org + before_script: [] + dependencies: + - compile-assets + script: + # Remove node modules from GitLab that may conflict with gitlab-ui + - rm -r node_modules + - git clone https://gitlab.com/gitlab-org/gitlab-ui.git + - cp public/assets/application-*.css gitlab-ui/styles/application.css + - cd gitlab-ui + - yarn install + - CSS_URL=./application.css yarn test + artifacts: + paths: + - tests/__image_snapshots__/ + karma: <<: *dedicated-no-docs-pull-cache-job <<: *use-pg diff --git a/package.json b/package.json index 86ee0de475e..251ab8789a5 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", "@gitlab/svgs": "^1.54.0", - "@gitlab/ui": "^2.0.4", + "@gitlab/ui": "^2.1.0", "apollo-boost": "^0.3.1", "apollo-client": "^2.5.1", "autosize": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index c045f313cff..332bb550e17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -600,6 +600,11 @@ js-levenshtein "^1.1.3" semver "^5.3.0" +"@babel/standalone@^7.0.0": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.3.4.tgz#b622c1e522acef91b2a14f22bdcdd4f935a1a474" + integrity sha512-4L9c5i4WlGqbrjOVX0Yp8TIR5cEiw1/tPYYZENW/iuO2uI6viY38U7zALidzNfGdZIwNc+A/AWqMEWKeScWkBg== + "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" @@ -658,12 +663,12 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.54.0.tgz#00320e845efd46716042cde0c348b990d4908daf" integrity sha512-DR17iy8TM5IbXEacqiDP0p8SuC/J8EL+98xbfVz5BKvRsPHpeZJQNlBF/petIV5d+KWM5A9v3GZTY7uMU7z/JQ== -"@gitlab/ui@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.0.4.tgz#ba86f6e5868ef7bc7f504cef9ca504c2d2f6bffd" - integrity sha512-dJ+KKpeqIAPYZtYZeciXhC/whNiGPVRjp5IgjQRddh3zsreqmfwQq58nSH7HepAAIepaqTe0UFuzBgrSWvVM6w== +"@gitlab/ui@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.1.0.tgz#815d108633dca4215addbb61037c92805e86f055" + integrity sha512-wvmhS3giW7VDktylshuYXaVxMXDNKxHX42lT+ges3cS4enIiSObErWKpWsncMBxWe59ynI4psoMjuX7Q9OM7kg== dependencies: - babel-standalone "^6.26.0" + "@babel/standalone" "^7.0.0" bootstrap-vue "^2.0.0-rc.11" copy-to-clipboard "^3.0.8" echarts "^4.2.0-rc.2" @@ -875,6 +880,8 @@ version "1.7.11" resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.11.tgz#d7267a1ee9c4594fd3f7e37298818ec65687db63" integrity sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw== + dependencies: + "@xtuc/long" "4.2.1" "@webassemblyjs/utf8@1.7.11": version "1.7.11" @@ -910,6 +917,11 @@ version "1.7.11" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz#b331e8e7cef8f8e2f007d42c3a36a0580a7d6ca7" integrity sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg== + dependencies: + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/helper-buffer" "1.7.11" + "@webassemblyjs/wasm-gen" "1.7.11" + "@webassemblyjs/wasm-parser" "1.7.11" "@webassemblyjs/wasm-parser@1.7.11": version "1.7.11" @@ -1498,11 +1510,6 @@ babel-runtime@^6.22.0: core-js "^2.4.0" regenerator-runtime "^0.11.0" -babel-standalone@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-standalone/-/babel-standalone-6.26.0.tgz#15fb3d35f2c456695815ebf1ed96fe7f015b6886" - integrity sha1-Ffs9NfLEVmlYFevx7Zb+fwFbaIY= - babylon@7.0.0-beta.19: version "7.0.0-beta.19" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.19.tgz#e928c7e807e970e0536b078ab3e0c48f9e052503" @@ -9662,6 +9669,9 @@ style-loader@^0.23.1: version "0.23.1" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== + dependencies: + loader-utils "^1.1.0" + schema-utils "^1.0.0" style-search@^0.1.0: version "0.1.0" -- GitLab From ad2f711adfe151f15f19559884cd84a8dd26e93e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 4 Mar 2019 23:19:19 -0800 Subject: [PATCH 66/78] Add frozen_string_literal to new files --- spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb | 2 ++ spec/policies/commit_policy_spec.rb | 2 ++ spec/validators/sha_validator_spec.rb | 2 ++ 3 files changed, 6 insertions(+) diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb index f81dbcf62da..9bfb1b13a2b 100644 --- a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb +++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe Gitlab::DependencyLinker::Parser::Gemfile do diff --git a/spec/policies/commit_policy_spec.rb b/spec/policies/commit_policy_spec.rb index 2259693cf01..41f6fb08426 100644 --- a/spec/policies/commit_policy_spec.rb +++ b/spec/policies/commit_policy_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe CommitPolicy do diff --git a/spec/validators/sha_validator_spec.rb b/spec/validators/sha_validator_spec.rb index b9242ef931e..dc1539cf318 100644 --- a/spec/validators/sha_validator_spec.rb +++ b/spec/validators/sha_validator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ShaValidator do -- GitLab From 434cb1d96f43c90f32427c60a82308307a9181e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Thu, 17 Jan 2019 20:41:53 +0100 Subject: [PATCH 67/78] Display only informaton visible to current user Display only labels and assignees of issues visible by the currently logged user Display only issues visible to user in the burndown chart --- app/models/concerns/milestoneish.rb | 12 +++ app/views/shared/milestones/_tabs.html.haml | 4 +- .../security-2774-milestones-detail.yml | 5 + spec/models/concerns/milestoneish_spec.rb | 95 ++++++++++++++++++- 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/security-2774-milestones-detail.yml diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 39372c4f68b..dff8b3af608 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -53,6 +53,18 @@ module Milestoneish end end + def issue_participants_visible_by_user(user) + User.joins(:issue_assignees) + .where('issue_assignees.issue_id' => issues_visible_to_user(user).select(:id)) + .distinct + end + + def issue_labels_visible_by_user(user) + Label.joins(:label_links) + .where('label_links.target_id' => issues_visible_to_user(user).select(:id), 'label_links.target_type' => 'Issue') + .distinct + end + def sorted_issues(user) issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority') end diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 55460acab8f..3b435847172 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -21,11 +21,11 @@ %li.nav-item = link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do Participants - %span.badge.badge-pill= milestone.participants.count + %span.badge.badge-pill= milestone.issue_participants_visible_by_user(current_user).count %li.nav-item = link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do Labels - %span.badge.badge-pill= milestone.labels.count + %span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count - issues = milestone.sorted_issues(current_user) - show_project_name = local_assigns.fetch(:show_project_name, false) diff --git a/changelogs/unreleased/security-2774-milestones-detail.yml b/changelogs/unreleased/security-2774-milestones-detail.yml new file mode 100644 index 00000000000..faf56fee01e --- /dev/null +++ b/changelogs/unreleased/security-2774-milestones-detail.yml @@ -0,0 +1,5 @@ +--- +title: Display only information visible to current user on the Milestone page +merge_request: +author: +type: security diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index 4647eecbdef..81ca5b638fe 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -9,8 +9,10 @@ describe Milestone, 'Milestoneish' do let(:admin) { create(:admin) } let(:project) { create(:project, :public) } let(:milestone) { create(:milestone, project: project) } - let!(:issue) { create(:issue, project: project, milestone: milestone) } - let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) } + let(:label1) { create(:label, project: project) } + let(:label2) { create(:label, project: project) } + let!(:issue) { create(:issue, project: project, milestone: milestone, assignees: [member], labels: [label1]) } + let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone, labels: [label2]) } let!(:security_issue_2) { create(:issue, :confidential, project: project, assignees: [assignee], milestone: milestone) } let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) } let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) } @@ -42,6 +44,95 @@ describe Milestone, 'Milestoneish' do end end + context 'attributes visibility' do + using RSpec::Parameterized::TableSyntax + + let(:users) do + { + anonymous: nil, + non_member: non_member, + guest: guest, + member: member, + assignee: assignee + } + end + + let(:project_visibility_levels) do + { + public: Gitlab::VisibilityLevel::PUBLIC, + internal: Gitlab::VisibilityLevel::INTERNAL, + private: Gitlab::VisibilityLevel::PRIVATE + } + end + + describe '#issue_participants_visible_by_user' do + where(:visibility, :user_role, :result) do + :public | nil | [:member] + :public | :non_member | [:member] + :public | :guest | [:member] + :public | :member | [:member, :assignee] + :internal | nil | [] + :internal | :non_member | [:member] + :internal | :guest | [:member] + :internal | :member | [:member, :assignee] + :private | nil | [] + :private | :non_member | [] + :private | :guest | [:member] + :private | :member | [:member, :assignee] + end + + with_them do + before do + project.update(visibility_level: project_visibility_levels[visibility]) + end + + it 'returns the proper participants' do + user = users[user_role] + participants = result.map { |role| users[role] } + + expect(milestone.issue_participants_visible_by_user(user)).to match_array(participants) + end + end + end + + describe '#issue_labels_visible_by_user' do + let(:labels) do + { + label1: label1, + label2: label2 + } + end + + where(:visibility, :user_role, :result) do + :public | nil | [:label1] + :public | :non_member | [:label1] + :public | :guest | [:label1] + :public | :member | [:label1, :label2] + :internal | nil | [] + :internal | :non_member | [:label1] + :internal | :guest | [:label1] + :internal | :member | [:label1, :label2] + :private | nil | [] + :private | :non_member | [] + :private | :guest | [:label1] + :private | :member | [:label1, :label2] + end + + with_them do + before do + project.update(visibility_level: project_visibility_levels[visibility]) + end + + it 'returns the proper participants' do + user = users[user_role] + expected_labels = result.map { |label| labels[label] } + + expect(milestone.issue_labels_visible_by_user(user)).to match_array(expected_labels) + end + end + end + end + describe '#sorted_merge_requests' do it 'sorts merge requests by label priority' do merge_request_1 = create(:labeled_merge_request, labels: [label_2], source_project: project, source_branch: 'branch_1', milestone: milestone) -- GitLab From 91805098ee05df1209b06ee752490ec7aa0c34b8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 5 Mar 2019 08:35:54 +0000 Subject: [PATCH 68/78] Scope gitlab:ui:visual pipeline job to css only changes --- .gitlab-ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fa47aea310d..853c10c5df6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -686,6 +686,11 @@ gitlab:ui:visual: - cd gitlab-ui - yarn install - CSS_URL=./application.css yarn test + only: + changes: + - app/assets/stylesheets/*.scss + - app/assets/stylesheets/**/*.scss + - app/assets/stylesheets/**/**/*.scss artifacts: paths: - tests/__image_snapshots__/ -- GitLab From 31469e1835fcf1f24d35538b60e6e9b09cf5ed7b Mon Sep 17 00:00:00 2001 From: Ramya Authappan Date: Tue, 5 Mar 2019 08:39:04 +0000 Subject: [PATCH 69/78] Script to generate Perf Testdata --- qa/.gitignore | 1 + qa/Gemfile | 1 + qa/Gemfile.lock | 3 + qa/Rakefile | 6 ++ qa/qa/support/api.rb | 10 ++ qa/qa/tools/generate_perf_testdata.rb | 141 ++++++++++++++++++++++++++ 6 files changed, 162 insertions(+) create mode 100644 qa/qa/tools/generate_perf_testdata.rb diff --git a/qa/.gitignore b/qa/.gitignore index 19ec17d0005..102f7e5e54d 100644 --- a/qa/.gitignore +++ b/qa/.gitignore @@ -1,2 +1,3 @@ tmp/ .ruby-version +urls.txt diff --git a/qa/Gemfile b/qa/Gemfile index f29006617ed..38e95ba2d65 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -9,3 +9,4 @@ gem 'selenium-webdriver', '~> 3.12' gem 'airborne', '~> 0.2.13' gem 'nokogiri', '~> 1.10.1' gem 'rspec-retry', '~> 0.6.1' +gem 'faker', '~> 1.6', '>= 1.6.6' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index c3d9f558c23..c9b0db6a272 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -32,6 +32,8 @@ GEM diff-lcs (1.3) domain_name (0.5.20170404) unf (>= 0.0.5, < 1.0.0) + faker (1.9.3) + i18n (>= 0.7) ffi (1.9.25) http-cookie (1.0.3) domain_name (~> 0.5) @@ -99,6 +101,7 @@ DEPENDENCIES airborne (~> 0.2.13) capybara (~> 2.16.1) capybara-screenshot (~> 1.0.18) + faker (~> 1.6, >= 1.6.6) nokogiri (~> 1.10.1) pry-byebug (~> 3.5.1) rake (~> 12.3.0) diff --git a/qa/Rakefile b/qa/Rakefile index 9a7b9c6bb35..b6ad09f9b00 100644 --- a/qa/Rakefile +++ b/qa/Rakefile @@ -1,5 +1,6 @@ require_relative 'qa/tools/revoke_all_personal_access_tokens' require_relative 'qa/tools/delete_subgroups' +require_relative 'qa/tools/generate_perf_testdata' desc "Revokes all personal access tokens" task :revoke_personal_access_tokens do @@ -10,3 +11,8 @@ desc "Deletes subgroups within a provided group" task :delete_subgroups do QA::Tools::DeleteSubgroups.new.run end + +desc "Generate Performance Testdata" +task :generate_perf_testdata do + QA::Tools::GeneratePerfTestdata.new.run +end diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 8aa7d6812ac..229bfb44fa5 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -20,6 +20,16 @@ module QA e.response end + def put(url, payload) + RestClient::Request.execute( + method: :put, + url: url, + payload: payload, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + e.response + end + def delete(url) RestClient::Request.execute( method: :delete, diff --git a/qa/qa/tools/generate_perf_testdata.rb b/qa/qa/tools/generate_perf_testdata.rb new file mode 100644 index 00000000000..ad515014794 --- /dev/null +++ b/qa/qa/tools/generate_perf_testdata.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'securerandom' +require 'faker' +require_relative '../../qa' +# This script generates testdata for Performance Testing. +# Required environment variables: PERSONAL_ACCESS_TOKEN and GITLAB_ADDRESS +# This job creates a urls.txt which contains a hash of all the URLs needed for Performance Testing +# Run `rake generate_perf_testdata` + +module QA + module Tools + class GeneratePerfTestdata + include Support::Api + + def initialize + raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] + raise ArgumentError, "Please provide PERSONAL_ACCESS_TOKEN" unless ENV['PERSONAL_ACCESS_TOKEN'] + + @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['PERSONAL_ACCESS_TOKEN']) + @group_name = "gitlab-qa-perf-sandbox-#{SecureRandom.hex(8)}" + @project_name = "my-test-project-#{SecureRandom.hex(8)}" + @urls = {} + end + + def run + STDOUT.puts 'Running...' + group_id = create_group + create_project(group_id) + create_branch + add_new_file + methods_arr = [ + method(:create_issues), + method(:create_todos), + method(:create_merge_requests), + method(:create_issue_with_500_discussions), + method(:create_mr_with_large_files) + ] + threads_arr = [] + + methods_arr.each do |m| + threads_arr << Thread.new {m.call} + end + + threads_arr.each(&:join) + STDOUT.puts "\nURLs: #{@urls}" + File.open("urls.txt", "w") { |file| file.puts @urls.to_s} + STDOUT.puts "\nDone" + end + + private + + def create_group + group_search_response = post Runtime::API::Request.new(@api_client, "/groups").url, "name=#{@group_name}&path=#{@group_name}" + group = JSON.parse(group_search_response.body) + @urls[:group_page] = group["web_url"] + group["id"] + end + + def create_project(group_id) + create_project_response = post Runtime::API::Request.new(@api_client, "/projects").url, "name=#{@project_name}&namespace_id=#{group_id}" + @urls[:project_page] = JSON.parse(create_project_response.body)["web_url"] + end + + def create_issues + 30.times do |i| + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/issues").url, "title=issue#{i}&description=desc#{i}" + end + @urls[:issues_list_page] = @urls[:project_page] + "/issues" + STDOUT.puts "Created Issues" + end + + def create_todos + 30.times do |i| + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/issues/#{i + 1}/todo").url, nil + end + @urls[:todos_page] = ENV['GITLAB_ADDRESS'] + "/dashboard/todos" + STDOUT.puts "Created todos" + end + + def create_merge_requests + 30.times do |i| + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests").url, "source_branch=branch#{i}&target_branch=master&title=MR#{i}" + end + @urls[:mr_list_page] = @urls[:project_page] + "/merge_requests" + STDOUT.puts "Created MRs" + end + + def add_new_file + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello.txt").url, "branch=master&commit_message=\"hello\"&content=\"my new content\"" + 30.times do |i| + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, "branch=branch#{i}&commit_message=\"hello\"&content=\"my new content\"" + end + STDOUT.puts "Added Files" + end + + def create_branch + 30.times do |i| + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/branches").url, "branch=branch#{i}&ref=master" + end + STDOUT.puts "Created branches" + end + + def create_issue_with_500_discussions + issue_id = 1 + 500.times do + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/issues/#{issue_id}/discussions").url, "body=\"Let us discuss\"" + end + @urls[:large_issue] = @urls[:project_page] + "/issues/#{issue_id}" + STDOUT.puts "Created Issue with 500 Discussions" + end + + def create_mr_with_large_files + content_arr = [] + 20.times do |i| + faker_line_arr = Faker::Lorem.sentences(1500) + content = faker_line_arr.join("\n\r") + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, "branch=master&commit_message=\"Add hello#{i}.txt\"&content=#{content}" + content_arr[i] = faker_line_arr + end + + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/branches").url, "branch=performance&ref=master" + + 20.times do |i| + missed_line_array = content_arr[i].each_slice(2).map(&:first) + content = missed_line_array.join("\n\rIm new!:D \n\r ") + put Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, "branch=performance&commit_message=\"Update hello#{i}.txt\"&content=#{content}" + end + + create_mr_response = post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests").url, "source_branch=performance&target_branch=master&title=Large_MR" + + iid = JSON.parse(create_mr_response.body)["iid"] + 500.times do + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions").url, "body=\"Let us discuss\"" + end + @urls[:large_mr] = JSON.parse(create_mr_response.body)["web_url"] + STDOUT.puts "Created MR with 500 Discussions and 20 Very Large Files" + end + end + end +end -- GitLab From a24eabb1782785ad0bcf2460c3fff67106598b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C5=82gorzata=20Ksionek?= Date: Tue, 5 Mar 2019 10:38:09 +0100 Subject: [PATCH 70/78] Resolve conflicts in group policy --- app/policies/group_policy.rb | 1 - spec/policies/group_policy_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 298769c0eb8..e74e5f008d7 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -53,7 +53,6 @@ class GroupPolicy < BasePolicy rule { admin }.enable :read_group rule { has_projects }.policy do - enable :read_group enable :read_list enable :read_label end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 4c31ff30fc6..0ad50c6f91f 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -114,13 +114,13 @@ describe GroupPolicy do project.add_developer(current_user) end - it { expect_allowed(:read_label) } + it { expect_allowed(:read_label, :read_list) } context 'in subgroups', :nested_groups do let(:subgroup) { create(:group, :private, parent: group) } let(:project) { create(:project, namespace: subgroup) } - it { expect_allowed(:read_label) } + it { expect_allowed(:read_label, :read_list) } end end -- GitLab From 55f76ce8f6010458988ccbc99201f6b74d11e238 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 5 Mar 2019 11:02:00 +0000 Subject: [PATCH 71/78] Revert "Merge branch 'winh-dequarantine-labels-autocomplete' into 'master'" This reverts merge request !25542 --- jest.config.js | 1 - spec/features/issues/gfm_autocomplete_spec.rb | 84 ++++++++++++++++- spec/frontend/gfm_auto_complete_spec.js | 94 +------------------ .../fixtures/autocomplete_sources.rb | 40 -------- 4 files changed, 85 insertions(+), 134 deletions(-) delete mode 100644 spec/javascripts/fixtures/autocomplete_sources.rb diff --git a/jest.config.js b/jest.config.js index 4e346005b8a..efbf2e602c1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,7 +18,6 @@ module.exports = { moduleNameMapper: { '^~(.*)$': '/app/assets/javascripts$1', '^ee(.*)$': '/ee/app/assets/javascripts$1', - '^fixtures(.*)$': '/spec/javascripts/fixtures$1', '^helpers(.*)$': '/spec/frontend/helpers$1', '^vendor(.*)$': '/vendor/assets/javascripts$1', '\\.(jpg|jpeg|png|svg)$': '/spec/frontend/__mocks__/file_mock.js', diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 8eb413bdd8d..986f3823275 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -278,7 +278,12 @@ describe 'GFM autocomplete', :js do end end - context 'labels' do + # This context has just one example in each contexts in order to improve spec performance. + context 'labels', :quarantine do + let!(:backend) { create(:label, project: project, title: 'backend') } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') } + it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do create(:label, project: project, title: label_xss_title) @@ -293,6 +298,83 @@ describe 'GFM autocomplete', :js do expect(find('.atwho-view-ul').text).to have_content('alert label') end end + + context 'when no labels are assigned' do + it 'shows labels' do + note = find('#note-body') + + # It should show all the labels on "~". + type(note, '~') + wait_for_requests + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show all the labels on "/label ~". + type(note, '/label ~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show all the labels on "/relabel ~". + type(note, '/relabel ~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show no labels on "/unlabel ~". + type(note, '/unlabel ~') + expect_labels(not_shown: [backend, bug, feature_proposal]) + end + end + + context 'when some labels are assigned' do + before do + issue.labels << [backend] + end + + it 'shows labels' do + note = find('#note-body') + + # It should show all the labels on "~". + type(note, '~') + wait_for_requests + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show only unset labels on "/label ~". + type(note, '/label ~') + expect_labels(shown: [bug, feature_proposal], not_shown: [backend]) + + # It should show all the labels on "/relabel ~". + type(note, '/relabel ~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show only set labels on "/unlabel ~". + type(note, '/unlabel ~') + expect_labels(shown: [backend], not_shown: [bug, feature_proposal]) + end + end + + context 'when all labels are assigned' do + before do + issue.labels << [backend, bug, feature_proposal] + end + + it 'shows labels' do + note = find('#note-body') + + # It should show all the labels on "~". + type(note, '~') + wait_for_requests + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show no labels on "/label ~". + type(note, '/label ~') + expect_labels(not_shown: [backend, bug, feature_proposal]) + + # It should show all the labels on "/relabel ~". + type(note, '/relabel ~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show all the labels on "/unlabel ~". + type(note, '/unlabel ~') + expect_labels(shown: [backend, bug, feature_proposal]) + end + end end shared_examples 'autocomplete suggestions' do diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index b79e6e0fe7b..c7008c780d6 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -6,21 +6,17 @@ import GfmAutoComplete from '~/gfm_auto_complete'; import 'vendor/jquery.caret'; import 'vendor/jquery.atwho'; -import { TEST_HOST } from 'helpers/test_constants'; -import labelsFixture from 'fixtures/autocomplete_sources/labels.json'; // eslint-disable-line import/no-unresolved - describe('GfmAutoComplete', () => { const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ fetchData: () => {}, }); let atwhoInstance; + let items; let sorterValue; describe('DefaultOptions.sorter', () => { describe('assets loading', () => { - let items; - beforeEach(() => { jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(true); @@ -65,7 +61,7 @@ describe('GfmAutoComplete', () => { atwhoInstance = { setting: {} }; const query = 'query'; - const items = []; + items = []; const searchKey = 'searchKey'; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey); @@ -254,90 +250,4 @@ describe('GfmAutoComplete', () => { ).toBe('
  • grp/proj#5 Some Issue
  • '); }); }); - - describe('labels', () => { - const dataSources = { - labels: `${TEST_HOST}/autocomplete_sources/labels`, - }; - - const allLabels = labelsFixture; - const assignedLabels = allLabels.filter(label => label.set); - const unassignedLabels = allLabels.filter(label => !label.set); - - let autocomplete; - let $textarea; - - beforeEach(() => { - autocomplete = new GfmAutoComplete(dataSources); - $textarea = $(''); - autocomplete.setup($textarea, { labels: true }); - }); - - afterEach(() => { - autocomplete.destroy(); - }); - - const triggerDropdown = text => { - $textarea - .trigger('focus') - .val(text) - .caret('pos', -1); - $textarea.trigger('keyup'); - - return new Promise(window.requestAnimationFrame); - }; - - const getDropdownItems = () => { - const dropdown = document.getElementById('at-view-labels'); - const items = dropdown.getElementsByTagName('li'); - return [].map.call(items, item => item.textContent.trim()); - }; - - const expectLabels = ({ input, output }) => - triggerDropdown(input).then(() => { - expect(getDropdownItems()).toEqual(output.map(label => label.title)); - }); - - describe('with no labels assigned', () => { - beforeEach(() => { - autocomplete.cachedData['~'] = [...unassignedLabels]; - }); - - it.each` - input | output - ${'~'} | ${unassignedLabels} - ${'/label ~'} | ${unassignedLabels} - ${'/relabel ~'} | ${unassignedLabels} - ${'/unlabel ~'} | ${[]} - `('$input shows $output.length labels', expectLabels); - }); - - describe('with some labels assigned', () => { - beforeEach(() => { - autocomplete.cachedData['~'] = allLabels; - }); - - it.each` - input | output - ${'~'} | ${allLabels} - ${'/label ~'} | ${unassignedLabels} - ${'/relabel ~'} | ${allLabels} - ${'/unlabel ~'} | ${assignedLabels} - `('$input shows $output.length labels', expectLabels); - }); - - describe('with all labels assigned', () => { - beforeEach(() => { - autocomplete.cachedData['~'] = [...assignedLabels]; - }); - - it.each` - input | output - ${'~'} | ${assignedLabels} - ${'/label ~'} | ${[]} - ${'/relabel ~'} | ${assignedLabels} - ${'/unlabel ~'} | ${assignedLabels} - `('$input shows $output.length labels', expectLabels); - }); - }); }); diff --git a/spec/javascripts/fixtures/autocomplete_sources.rb b/spec/javascripts/fixtures/autocomplete_sources.rb deleted file mode 100644 index c117fb7cd24..00000000000 --- a/spec/javascripts/fixtures/autocomplete_sources.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: :controller do - include JavaScriptFixturesHelpers - - set(:admin) { create(:admin) } - set(:group) { create(:group, name: 'frontend-fixtures') } - set(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') } - set(:issue) { create(:issue, project: project) } - - before(:all) do - clean_frontend_fixtures('autocomplete_sources/') - end - - before do - sign_in(admin) - end - - it 'autocomplete_sources/labels.json' do |example| - issue.labels << create(:label, project: project, title: 'bug') - issue.labels << create(:label, project: project, title: 'critical') - - create(:label, project: project, title: 'feature') - create(:label, project: project, title: 'documentation') - - get :labels, - format: :json, - params: { - namespace_id: group.path, - project_id: project.path, - type: issue.class.name, - type_id: issue.id - } - - expect(response).to be_success - store_frontend_fixture(response, example.description) - end -end -- GitLab From 23d26afa2eb2c02e616b5a06c0289a8e56caf389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Tue, 5 Mar 2019 12:41:39 +0100 Subject: [PATCH 72/78] Make sure MR and milestone projects are same --- spec/models/ci/pipeline_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 332278ce077..d0b42d103a5 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -520,7 +520,7 @@ describe Ci::Pipeline, :mailer do end let(:assignee) { create(:user) } - let(:milestone) { create(:milestone) } + let(:milestone) { create(:milestone, project: project) } let(:labels) { create_list(:label, 2) } it 'exposes merge request pipeline variables' do -- GitLab From ab18641f566eb8ff9a9ba44737e926169af8a519 Mon Sep 17 00:00:00 2001 From: Dany Jupille Date: Tue, 5 Mar 2019 12:20:08 +0000 Subject: [PATCH 73/78] Fix large table horizontal scroll and prevent side-by-side tables --- app/assets/stylesheets/framework/mixins.scss | 7 +------ changelogs/unreleased/table-fix-scroll-and-block.yml | 5 +++++ 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/table-fix-scroll-and-block.yml diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index fda7b6d5467..ba3b0906e28 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -26,16 +26,11 @@ */ @mixin markdown-table { width: auto; - display: inline-block; + display: block; overflow-x: auto; border: 0; border-color: $gl-gray-100; - @supports (width: fit-content) { - display: block; - width: fit-content; - } - tr { th { border-bottom: solid 2px $gl-gray-100; diff --git a/changelogs/unreleased/table-fix-scroll-and-block.yml b/changelogs/unreleased/table-fix-scroll-and-block.yml new file mode 100644 index 00000000000..e6def0468b8 --- /dev/null +++ b/changelogs/unreleased/table-fix-scroll-and-block.yml @@ -0,0 +1,5 @@ +--- +title: Fix large table horizontal scroll and prevent side-by-side tables +merge_request: 25520 +author: Dany Jupille +type: fixed -- GitLab From 084d16a8fa4edf29a25969a17c70efd9832fcc49 Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Tue, 5 Mar 2019 12:55:30 +0000 Subject: [PATCH 74/78] Docs: Fix all anchors in /user docs --- doc/user/group/clusters/index.md | 2 +- doc/user/group/index.md | 2 +- doc/user/group/subgroups/index.md | 12 ++++++------ doc/user/profile/index.md | 2 +- doc/user/project/clusters/index.md | 4 ++-- doc/user/project/import/svn.md | 2 +- doc/user/project/index.md | 2 +- .../integrations/mattermost_slash_commands.md | 2 +- doc/user/project/integrations/prometheus.md | 2 +- doc/user/project/pages/getting_started_part_one.md | 2 +- doc/user/project/pages/getting_started_part_three.md | 6 +++--- doc/user/project/pages/index.md | 2 +- doc/user/project/pages/introduction.md | 6 +++--- doc/user/project/pipelines/job_artifacts.md | 2 +- doc/user/project/repository/branches/index.md | 2 +- doc/user/search/index.md | 2 +- 16 files changed, 26 insertions(+), 26 deletions(-) diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 7422a30ff3c..8cdfb13a97b 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -56,7 +56,7 @@ group. That way you can have different clusters for different environments, like dev, staging, production, etc. Add another cluster similar to the first one and make sure to -[set an environment scope](#environment-scopes) that will +[set an environment scope](#environment-scopes-premium) that will differentiate the new cluster from the rest. ## Base domain diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 300e0115c60..67b93ed6666 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -154,7 +154,7 @@ There are two different ways to add a new project to a group: ## Transfer projects into groups -Learn how to [transfer a project into a group](../project/index.md#transfer-an-existing-project-into-a-group). +Learn how to [transfer a project into a group](../project/settings/index.md#transferring-an-existing-project-into-another-namespace). ## Sharing a project with a group diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index b6f8f55978b..3cecefe11f5 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -1,8 +1,8 @@ # Subgroups NOTE: **Note:** -[Introduced][ce-2772] in GitLab 9.0. Not available when using MySQL as external -database (support removed in GitLab 9.3 [due to performance reasons][issue]). +[Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2772) in GitLab 9.0. Not available when using MySQL as external +database (support removed in GitLab 9.3 [due to performance reasons](https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600)). With subgroups (aka nested groups or hierarchical groups) you can have up to 20 levels of nested groups, which among other things can help you to: @@ -13,7 +13,7 @@ up to 20 levels of nested groups, which among other things can help you to: - **Organize large projects.** For large projects, subgroups makes it potentially easier to separate permissions on parts of the source code. - **Make it easier to manage people and control visibility.** Give people - different [permissions][] depending on their group [membership](#membership). + different [permissions](../../permissions.md#group-members-permissions) depending on their group [membership](#membership). ## Database Requirements @@ -80,9 +80,9 @@ structure. NOTE: **Note:** You need to be an Owner of a group in order to be able to create a subgroup. For -more information check the [permissions table][permissions]. +more information check the [permissions table](../../permissions.md#group-members-permissions). For a list of words that are not allowed to be used as group names see the -[reserved names][reserved]. +[reserved names](../../reserved_names.md). Users can always create subgroups if they are explicitly added as an Owner to a parent group even if group creation is disabled by an administrator in their settings. @@ -176,6 +176,6 @@ Here's a list of what you can't do with subgroups: `group/subgroup01/subgroup03`. [ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 -[permissions]: ../../permissions.md#group +[permissions]: ../../permissions.md#group-members-permissions [reserved]: ../../reserved_names.md [issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600 diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index a2b15d058d7..b216b9f255c 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -39,7 +39,7 @@ From there, you can: - Manage [personal access tokens](personal_access_tokens.md) to access your account via API and authorized applications - Add and delete emails linked to your account - Choose which email to use for notifications, web-based commits, and display on your public profile -- Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH +- Manage [SSH keys](../../ssh/README.md) to access your account via SSH - Manage your [preferences](preferences.md#syntax-highlighting-theme) to customize your own GitLab experience - [View your active sessions](active_sessions.md) and revoke any of them if necessary diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 0c514e005b2..6e64053a6ca 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -88,7 +88,7 @@ To add an existing Kubernetes cluster to your project: 1. Click **Add an existing Kubernetes cluster** and fill in the details: - **Kubernetes cluster name** (required) - The name you wish to give the cluster. - **Environment scope** (required) - The - [associated environment](#setting-the-environment-scope) to this cluster. + [associated environment](#setting-the-environment-scope-premium) to this cluster. - **API URL** (required) - It's the URL that GitLab uses to access the Kubernetes API. Kubernetes exposes several APIs, we want the "base" URL that is common to all of them, @@ -473,7 +473,7 @@ project. That way you can have different clusters for different environments, like dev, staging, production, etc. Simply add another cluster, like you did the first time, and make sure to -[set an environment scope](#setting-the-environment-scope) that will +[set an environment scope](#setting-the-environment-scope-premium) that will differentiate the new cluster with the rest. ## Setting the environment scope **[PREMIUM]** diff --git a/doc/user/project/import/svn.md b/doc/user/project/import/svn.md index a5923986292..4825b005a85 100644 --- a/doc/user/project/import/svn.md +++ b/doc/user/project/import/svn.md @@ -8,7 +8,7 @@ between the two, for more information consult your favorite search engine. There are two approaches to SVN to Git migration: -1. [Git/SVN Mirror](#smooth-migration-with-a-git-svn-mirror-using-subgit) which: +1. [Git/SVN Mirror](#smooth-migration-with-a-gitsvn-mirror-using-subgit) which: - Makes the GitLab repository to mirror the SVN project. - Git and SVN repositories are kept in sync; you can use either one. - Smoothens the migration process and allows to manage migration risks. diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 0dc50d28cb0..4148310dc98 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -137,7 +137,7 @@ and Git push/pull redirects. Depending on the situation, different things apply. When [renaming a user](../profile/index.md#changing-your-username), -[changing a group path](../group/index.md#changing-a-group-s-path) or [renaming a repository](settings/index.md#renaming-a-repository): +[changing a group path](../group/index.md#changing-a-groups-path) or [renaming a repository](settings/index.md#renaming-a-repository): - Existing web URLs for the namespace and anything under it (e.g., projects) will redirect to the new URLs. diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index e031dcad2c3..9c69437537a 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -152,7 +152,7 @@ trigger word followed by help. Example: /gitlab help ## Permissions The permissions to run the [available commands](#available-slash-commands) derive from -the [permissions you have on the project](../../permissions.md#project). +the [permissions you have on the project](../../permissions.md#project-members-permissions). ## Further reading diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index ed289b0c4eb..26989e2a8a4 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -120,7 +120,7 @@ If the "No data found" screen continues to appear, it could be due to: - No successful deployments have occurred to this environment. - Prometheus does not have performance data for this environment, or the metrics are not labeled correctly. To test this, connect to the Prometheus server and - [run a query](#gitlab-prometheus-queries), replacing `$CI_ENVIRONMENT_SLUG` + [run a query](prometheus_library/kubernetes.html#metrics-supported), replacing `$CI_ENVIRONMENT_SLUG` with the name of your environment. [autodeploy]: ../../../ci/autodeploy/index.md diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 9a95fb70964..f1e2771dcb9 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -59,7 +59,7 @@ which is highly recommendable and much faster than hardcoding. If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a -[subdomain of `namespace.gitlab.io`](introduction.md#gitlab-pages-on-gitlab-com). +[subdomain of `namespace.gitlab.io`](introduction.md#gitlab-pages-on-gitlabcom). The `namespace` is defined by your username on GitLab.com, or the group name you created this project under. diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index daae2f4b5a3..756b8b698c7 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -20,13 +20,13 @@ you should consider first, which we'll cover in this guide: 1. Either if you're adding a **root domain** or a **subdomain**, for which you'll need to set up [DNS records](#dns-records) -1. Whether you want to add an [SSL/TLS certificate](#ssl-tls-certificates) or not +1. Whether you want to add an [SSL/TLS certificate](#ssltls-certificates) or not To finish the association, you need to [add your domain to your project's Pages settings](#add-your-custom-domain-to-gitlab-pages-settings). Let's start from the beginning with [DNS records](#dns-records). If you already know how they work and want to skip the introduction to DNS, -you may be interested in skipping it until the [TL;DR](#tl-dr) section below. +you may be interested in skipping it until the [TL;DR](#tldr) section below. ### DNS Records @@ -148,7 +148,7 @@ verify your domain's ownership with a TXT record: Once you've set the DNS record, you'll need navigate to your project's **Setting > Pages** and click **+ New domain** to add your custom domain to -GitLab Pages. You can choose whether to add an [SSL/TLS certificate](#ssl-tls-certificates) +GitLab Pages. You can choose whether to add an [SSL/TLS certificate](#ssltls-certificates) to make your website accessible under HTTPS or leave it blank. If don't add a certificate, your site will be accessible only via HTTP: diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index e0b78753e21..ee471fa6534 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -141,7 +141,7 @@ You can also take some optional further steps: _Advanced options:_ - [Use a custom domain](getting_started_part_three.md#adding-your-custom-domain-to-gitlab-pages) -- Apply [SSL/TLS certification](getting_started_part_three.md#ssl-tls-certificates) to your custom domain +- Apply [SSL/TLS certification](getting_started_part_three.md#ssltls-certificates) to your custom domain ## Explore GitLab Pages diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index 23eb88fd305..6bb58689f38 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -11,7 +11,7 @@ With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can deploy static pages for your individual projects, your user or your group. -Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific +Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlabcom) for specific information, if you are using GitLab.com to host your website. ## Getting started with GitLab Pages domains @@ -410,7 +410,7 @@ file for both the `/data` and `/data/` URL paths. ### Add a custom domain to your Pages website For a complete guide on Pages domains, read through the article -[GitLab Pages from A to Z: Part 3 - Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md#setting-up-custom-domains-dns-records-and-ssl-tls-certificates) +[GitLab Pages from A to Z: Part 3 - GitLab Pages custom domains and SSL/TLS Certificates](getting_started_part_three.md) If this setting is enabled by your GitLab administrator, you should be able to see the **New Domain** button when visiting your project's settings through the @@ -451,7 +451,7 @@ private key when adding a new domain. ![Pages upload cert](img/pages_upload_cert.png) For a complete guide on Pages domains, read through the article -[GitLab Pages from A to Z: Part 3 - Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md#setting-up-custom-domains-dns-records-and-ssl-tls-certificates) +[GitLab Pages from A to Z: Part 3 - GitLab Pages custom domains and SSL/TLS Certificates](getting_started_part_three.md) ### Custom error codes pages diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index bf939dbdaa3..8b57129c9e1 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -38,7 +38,7 @@ turn are defined with the `paths` keyword. All paths to files and directories are relative to the repository that was cloned during the build. These uploaded artifacts will be kept in GitLab for 1 week as defined by the `expire_in` definition. You have the option to keep the artifacts from expiring via the -[web interface](#browsing-job-artifacts). If the expiry time is not defined, +[web interface](#browsing-artifacts). If the expiry time is not defined, it defaults to the [instance wide setting](../../admin_area/settings/continuous_integration.md#default-artifacts-expiration-core-only). diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md index f05554ffc5b..13e4f2ce163 100644 --- a/doc/user/project/repository/branches/index.md +++ b/doc/user/project/repository/branches/index.md @@ -11,7 +11,7 @@ Read through GiLab's branching documentation: See also: - [Branches API](../../../../api/branches.md), for information on operating on repository branches using the GitLab API. -- [GitLab Flow](../../../../university/training/gitlab_flow.md#gitlab-flow). Use the best of GitLab for your branching strategies. +- [GitLab Flow](../../../../university/training/gitlab_flow.md). Use the best of GitLab for your branching strategies. - [Getting started with Git](../../../../topics/git/index.md) and GitLab. ## Default branch diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 770cef42995..705983cce2a 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -96,7 +96,7 @@ quickly access issues and merge requests created or assigned to you within that Your [todos](../../workflow/todos.md#gitlab-todos) can be searched by "to do" and "done". You can [filter](../../workflow/todos.md#filtering-your-todos) them per project, author, type, and action. Also, you can sort them by -[**Label priority**](../../user/project/labels.md#prioritize-labels), +[**Label priority**](../../user/project/labels.md#label-priority), **Last created** and **Oldest created**. ## Projects -- GitLab From 9c8dc7ddf61eff16d56579a8818bb866ca3e3e5e Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Tue, 5 Mar 2019 13:06:32 +0000 Subject: [PATCH 75/78] Docs: Fix broken anchors in development docs --- doc/articles/index.md | 4 ++-- doc/development/changelog.md | 22 ++++++------------- .../contributing/merge_request_workflow.md | 2 +- doc/development/documentation/index.md | 4 ++-- .../site_architecture/global_nav.md | 2 +- doc/development/documentation/styleguide.md | 4 ++-- doc/development/ee_features.md | 6 ++--- doc/development/fe_guide/architecture.md | 7 ++---- doc/development/fe_guide/vue.md | 4 ++-- .../testing_guide/best_practices.md | 2 +- .../testing_guide/testing_levels.md | 4 ++-- 11 files changed, 24 insertions(+), 37 deletions(-) diff --git a/doc/articles/index.md b/doc/articles/index.md index 87ee17bb6de..162db11d6ac 100644 --- a/doc/articles/index.md +++ b/doc/articles/index.md @@ -4,8 +4,8 @@ comments: false # Technical articles list (deprecated) -[Technical articles](../development/documentation/index.md#technical-articles) are -topic-related documentation, written with an user-friendly approach and language, aiming +Technical articles are +topic-related documentation, written with a user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives. The list of technical articles was [deprecated](https://gitlab.com/gitlab-org/gitlab-ce/issues/41138) in favor of having them linked from their topic-related documentation: diff --git a/doc/development/changelog.md b/doc/development/changelog.md index cd0a1f46d27..6efed36edf0 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -135,21 +135,13 @@ If you're working on the GitLab EE repository, the entry will be added to | Argument | Shorthand | Purpose | | ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| [`--amend`] | | Amend the previous commit | -| [`--force`] | `-f` | Overwrite an existing entry | -| [`--merge-request`] | `-m` | Set merge request ID | -| [`--dry-run`] | `-n` | Don't actually write anything, just print | -| [`--git-username`] | `-u` | Use Git user.name configuration as the author | -| [`--type`] | `-t` | The category of the change, valid options are: `added`, `fixed`, `changed`, `deprecated`, `removed`, `security`, `performance`, `other` | -| [`--help`] | `-h` | Print help message | - -[`--amend`]: #-amend -[`--force`]: #-force-or-f -[`--merge-request`]: #-merge-request-or-m -[`--dry-run`]: #-dry-run-or-n -[`--git-username`]: #-git-username-or-u -[`--type`]: #-type-or-t -[`--help`]: #-help +| [`--amend`](#--amend) | | Amend the previous commit | +| [`--force`](#--force-or--f) | `-f` | Overwrite an existing entry | +| [`--merge-request`](#--merge-request-or--m) | `-m` | Set merge request ID | +| [`--dry-run`](#--dry-run-or--n) | `-n` | Don't actually write anything, just print | +| [`--git-username`](#--git-username-or--u) | `-u` | Use Git user.name configuration as the author | +| [`--type`](#--type-or--t) | `-t` | The category of the change, valid options are: `added`, `fixed`, `changed`, `deprecated`, `removed`, `security`, `performance`, `other` | +| `--help` | `-h` | Print help message | ##### `--amend` diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 4e766f37871..8b14c3b20ea 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -179,7 +179,7 @@ the feature you contribute through all of these steps. 1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant 1. Community questions answered 1. Answers to questions radiated (in docs/wiki/support etc.) -1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-or-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions +1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-at-the-system-level-aka-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions If you add a dependency in GitLab (such as an operating system package) please consider updating the following and note the applicability of each in your diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index 652fe3ea711..a4da34a50ce 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -511,10 +511,10 @@ Currently, the following tests are in place: 1. `docs lint`: Check that all internal (relative) links work correctly and that all cURL examples in API docs use the full switches. It's recommended - to [check locally](#previewing-locally) before pushing to GitLab by executing the command + to [check locally](#previewing-the-changes-live) before pushing to GitLab by executing the command `bundle exec nanoc check internal_links` on your local [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs) directory. -1. [`ee_compat_check`](../automatic_ce_ee_merge.md#avoiding-ce-gt-ee-merge-conflicts-beforehand) (runs on CE only): +1. [`ee_compat_check`](../automatic_ce_ee_merge.md#avoiding-ce-ee-merge-conflicts-beforehand) (runs on CE only): When you submit a merge request to GitLab Community Edition (CE), there is this additional job that runs against Enterprise Edition (EE) and checks if your changes can apply cleanly to the EE codebase. diff --git a/doc/development/documentation/site_architecture/global_nav.md b/doc/development/documentation/site_architecture/global_nav.md index 0aa3c41a225..f2f4f5f0e1c 100644 --- a/doc/development/documentation/site_architecture/global_nav.md +++ b/doc/development/documentation/site_architecture/global_nav.md @@ -62,7 +62,7 @@ the consent of one of the technical writers. The global nav is built from two files: - [Data](#data-file) -- [Layout](#layout-file) +- [Layout](#layout-file-logic) The data file feeds the layout with the links to the docs. The layout organizes the data among the nav in containers properly [styled](#css-classes). diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index cd38721e3ab..0c51d3832aa 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -36,7 +36,7 @@ gem will support all [GFM markup](../../user/markdown.md) in the future. For now use regular markdown markup, following the rules on this style guide. For a complete Kramdown reference, check the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). Use Kramdown markup wisely: do not overuse its specific markup (e.g., `{:.class}`) as it will not render properly in -[`/help`](#gitlab-help). +[`/help`](index.md#gitlab-help). ## Content @@ -630,7 +630,7 @@ In this case: - The code blocks are indented one or more spaces under the list item to render correctly. - Different highlighting languages are used for each config in the code block. -- The [references](#references) guide is used for reconfigure/restart. +- The [GitLab Restart](#gitlab-restart) section is used to explain a required restart/reconfigure of GitLab. ## API diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 17da4c51033..41a64044c68 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -161,7 +161,7 @@ still having access the class's implementation with `super`. There are a few gotchas with it: -- you should always [`extend ::Gitlab::Utils::Override`] and use `override` to +- you should always [`extend ::Gitlab::Utils::Override`](utilities.md#overridehttpsgitlabcomgitlab-orggitlab-ceblobmasterlibgitlabutilsoverriderb) and use `override` to guard the "overrider" method to ensure that if the method gets renamed in CE, the EE override won't be silently forgotten. - when the "overrider" would add a line in the middle of the CE @@ -273,8 +273,6 @@ module EE end ``` -[`extend ::Gitlab::Utils::Override`]: utilities.md#override - ##### Overriding CE class methods The same applies to class methods, except we want to use @@ -977,7 +975,7 @@ if (renderIfEE) { To separate EE-specific styles in SCSS files, if a component you're adding styles for is limited to only EE, it is better to have a separate SCSS file in appropriate directory within `app/assets/stylesheets`. -See [backporting changes](#backporting-changes) for instructions on how to merge changes safely. +See [backporting changes](#backporting-changes-from-EE-to-CE) for instructions on how to merge changes safely. In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill, e.g. a text style of some component is different for EE. In such cases, diff --git a/doc/development/fe_guide/architecture.md b/doc/development/fe_guide/architecture.md index aebb22caa15..c67389b169e 100644 --- a/doc/development/fe_guide/architecture.md +++ b/doc/development/fe_guide/architecture.md @@ -11,12 +11,9 @@ Architectural decisions should be accessible to everyone, so please document them in the relevant Merge Request discussion or by updating our documentation when appropriate. -You can find the Frontend Architecture experts on the [team page][team-page]. +You can find the Frontend Architecture experts on the [team page](https://about.gitlab.com/team). ## Examples You can find documentation about the desired architecture for a new feature -built with Vue.js [here][vue-section]. - -[team-page]: https://about.gitlab.com/team -[vue-section]: vue.md#frontend.html#how-to-build-a-new-feature-with-vue-js +built with Vue.js [here](vue.md). diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 3cd70bd63fa..435fdf39fb4 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -122,7 +122,7 @@ Check this [page](vuex.md) for more details. ## Style guide -Please refer to the Vue section of our [style guide](style_guide_js.md#vue-js) +Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs) for best practices while writing your Vue components and templates. ## Testing Vue Components @@ -132,7 +132,7 @@ Each Vue component has a unique output. This output is always present in the ren Although we can test each method of a Vue component individually, our goal must be to test the output of the render/template function, which represents the state at all times. -Make use of the [axios mock adapter](axios.md#mock-axios-response-on-tests) to mock data returned. +Make use of the [axios mock adapter](axios.md#mock-axios-response-in-tests) to mock data returned. Here's how we would test the Todo App above: diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 2bd8332bf93..cfe0e6f70fc 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -40,7 +40,7 @@ bundle exec rspec spec/[path]/[to]/[spec].rb to separate phases. - Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` - Don't assert against the absolute value of a sequence-generated attribute (see - [Gotchas](../gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). + [Gotchas](../gotchas.md#do-not-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). - Don't supply the `:each` argument to hooks since it's the default. - On `before` and `after` hooks, prefer it scoped to `:context` over `:all` - When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element, diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index a7a3459719b..5d46833a1e2 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -46,7 +46,7 @@ They're useful to test permissions, redirections, what view is rendered etc. | `app/mailers/` | `spec/mailers/` | RSpec | | | `lib/api/` | `spec/requests/api/` | RSpec | | | `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | | -| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. | +| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Karma JavaScript test suite](frontend_testing.md#karma-test-suite) section. | ### About controller tests @@ -210,7 +210,7 @@ trade-off: - Integration tests are a bit more expensive, but don't abuse them. A system test is often better than an integration test that is stubbing a lot of internals. - System tests are expensive (compared to unit tests), even more if they require - a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed) + a JavaScript driver. Make sure to follow the guidelines in the [Speed](best_practices.md#test-speed) section. Another way to see it is to think about the "cost of tests", this is well -- GitLab From b3506d2f1e49c76600e4c07cf837758609bcaabe Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 5 Mar 2019 14:12:00 +0000 Subject: [PATCH 76/78] Revert "Merge branch 'gitlab-ui-visual' into 'master'" This reverts commit 34293b5af83af03e8c814f488be8c31e37ff5be3, reversing changes made to fdcfae2a92ac105d458e6f9617c1f22229ef34b6. Conflicts: .gitlab-ci.yml --- .gitlab-ci.yml | 23 ----------------------- package.json | 2 +- yarn.lock | 30 ++++++++++-------------------- 3 files changed, 11 insertions(+), 44 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 853c10c5df6..2329c9c6edc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -672,29 +672,6 @@ gitlab:assets:compile: - docker - gitlab-org -gitlab:ui:visual: - tags: - - gitlab-org - before_script: [] - dependencies: - - compile-assets - script: - # Remove node modules from GitLab that may conflict with gitlab-ui - - rm -r node_modules - - git clone https://gitlab.com/gitlab-org/gitlab-ui.git - - cp public/assets/application-*.css gitlab-ui/styles/application.css - - cd gitlab-ui - - yarn install - - CSS_URL=./application.css yarn test - only: - changes: - - app/assets/stylesheets/*.scss - - app/assets/stylesheets/**/*.scss - - app/assets/stylesheets/**/**/*.scss - artifacts: - paths: - - tests/__image_snapshots__/ - karma: <<: *dedicated-no-docs-pull-cache-job <<: *use-pg diff --git a/package.json b/package.json index 251ab8789a5..86ee0de475e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", "@gitlab/svgs": "^1.54.0", - "@gitlab/ui": "^2.1.0", + "@gitlab/ui": "^2.0.4", "apollo-boost": "^0.3.1", "apollo-client": "^2.5.1", "autosize": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 332bb550e17..c045f313cff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -600,11 +600,6 @@ js-levenshtein "^1.1.3" semver "^5.3.0" -"@babel/standalone@^7.0.0": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.3.4.tgz#b622c1e522acef91b2a14f22bdcdd4f935a1a474" - integrity sha512-4L9c5i4WlGqbrjOVX0Yp8TIR5cEiw1/tPYYZENW/iuO2uI6viY38U7zALidzNfGdZIwNc+A/AWqMEWKeScWkBg== - "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" @@ -663,12 +658,12 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.54.0.tgz#00320e845efd46716042cde0c348b990d4908daf" integrity sha512-DR17iy8TM5IbXEacqiDP0p8SuC/J8EL+98xbfVz5BKvRsPHpeZJQNlBF/petIV5d+KWM5A9v3GZTY7uMU7z/JQ== -"@gitlab/ui@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.1.0.tgz#815d108633dca4215addbb61037c92805e86f055" - integrity sha512-wvmhS3giW7VDktylshuYXaVxMXDNKxHX42lT+ges3cS4enIiSObErWKpWsncMBxWe59ynI4psoMjuX7Q9OM7kg== +"@gitlab/ui@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.0.4.tgz#ba86f6e5868ef7bc7f504cef9ca504c2d2f6bffd" + integrity sha512-dJ+KKpeqIAPYZtYZeciXhC/whNiGPVRjp5IgjQRddh3zsreqmfwQq58nSH7HepAAIepaqTe0UFuzBgrSWvVM6w== dependencies: - "@babel/standalone" "^7.0.0" + babel-standalone "^6.26.0" bootstrap-vue "^2.0.0-rc.11" copy-to-clipboard "^3.0.8" echarts "^4.2.0-rc.2" @@ -880,8 +875,6 @@ version "1.7.11" resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.11.tgz#d7267a1ee9c4594fd3f7e37298818ec65687db63" integrity sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw== - dependencies: - "@xtuc/long" "4.2.1" "@webassemblyjs/utf8@1.7.11": version "1.7.11" @@ -917,11 +910,6 @@ version "1.7.11" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz#b331e8e7cef8f8e2f007d42c3a36a0580a7d6ca7" integrity sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg== - dependencies: - "@webassemblyjs/ast" "1.7.11" - "@webassemblyjs/helper-buffer" "1.7.11" - "@webassemblyjs/wasm-gen" "1.7.11" - "@webassemblyjs/wasm-parser" "1.7.11" "@webassemblyjs/wasm-parser@1.7.11": version "1.7.11" @@ -1510,6 +1498,11 @@ babel-runtime@^6.22.0: core-js "^2.4.0" regenerator-runtime "^0.11.0" +babel-standalone@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-standalone/-/babel-standalone-6.26.0.tgz#15fb3d35f2c456695815ebf1ed96fe7f015b6886" + integrity sha1-Ffs9NfLEVmlYFevx7Zb+fwFbaIY= + babylon@7.0.0-beta.19: version "7.0.0-beta.19" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.19.tgz#e928c7e807e970e0536b078ab3e0c48f9e052503" @@ -9669,9 +9662,6 @@ style-loader@^0.23.1: version "0.23.1" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== - dependencies: - loader-utils "^1.1.0" - schema-utils "^1.0.0" style-search@^0.1.0: version "0.1.0" -- GitLab From 59afa80ae08767cd8766eb0104c82e4aa7c27ed9 Mon Sep 17 00:00:00 2001 From: Martin Wortschack Date: Tue, 5 Mar 2019 14:14:11 +0000 Subject: [PATCH 77/78] Add SyntaxHighlightFilter to markup pipeline --- changelogs/unreleased/enable-markup-highlighting.yml | 5 +++++ lib/banzai/pipeline/markup_pipeline.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/enable-markup-highlighting.yml diff --git a/changelogs/unreleased/enable-markup-highlighting.yml b/changelogs/unreleased/enable-markup-highlighting.yml new file mode 100644 index 00000000000..33fcf784f7d --- /dev/null +++ b/changelogs/unreleased/enable-markup-highlighting.yml @@ -0,0 +1,5 @@ +--- +title: Enable syntax highlighting to other supported markups +merge_request: 25761 +author: +type: other diff --git a/lib/banzai/pipeline/markup_pipeline.rb b/lib/banzai/pipeline/markup_pipeline.rb index db79a22549c..ceba082cd4f 100644 --- a/lib/banzai/pipeline/markup_pipeline.rb +++ b/lib/banzai/pipeline/markup_pipeline.rb @@ -7,7 +7,8 @@ module Banzai @filters ||= FilterArray[ Filter::SanitizationFilter, Filter::ExternalLinkFilter, - Filter::PlantumlFilter + Filter::PlantumlFilter, + Filter::SyntaxHighlightFilter ] end end -- GitLab From 79c421105013c3694679fefe3c1c1d4cb30446fc Mon Sep 17 00:00:00 2001 From: Igor Drozdov Date: Tue, 5 Mar 2019 15:15:22 +0000 Subject: [PATCH 78/78] Display the correct number of MRs a user has access to --- app/models/concerns/milestoneish.rb | 14 +- app/models/project_feature.rb | 15 +- app/policies/project_policy.rb | 2 +- .../shared/milestones/_milestone.html.haml | 2 +- app/views/shared/milestones/_tabs.html.haml | 2 +- .../security-id-fix-mr-visibility.yml | 5 + spec/finders/merge_requests_finder_spec.rb | 508 +++++++++++------- 7 files changed, 335 insertions(+), 213 deletions(-) create mode 100644 changelogs/unreleased/security-id-fix-mr-visibility.yml diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index dff8b3af608..e65bbb8ca07 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -46,13 +46,6 @@ module Milestoneish end end - def merge_requests_visible_to_user(user) - memoize_per_user(user, :merge_requests_visible_to_user) do - MergeRequestsFinder.new(user, {}) - .execute.where(milestone_id: milestoneish_id) - end - end - def issue_participants_visible_by_user(user) User.joins(:issue_assignees) .where('issue_assignees.issue_id' => issues_visible_to_user(user).select(:id)) @@ -73,6 +66,13 @@ module Milestoneish merge_requests_visible_to_user(user).sort_by_attribute('label_priority') end + def merge_requests_visible_to_user(user) + memoize_per_user(user, :merge_requests_visible_to_user) do + MergeRequestsFinder.new(user, issues_finder_params) + .execute.where(milestone_id: milestoneish_id) + end + end + def upcoming? start_date && start_date.future? end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index f700090a493..e6787236c4e 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -76,7 +76,7 @@ class ProjectFeature < ActiveRecord::Base # This feature might not be behind a feature flag at all, so default to true return false unless ::Feature.enabled?(feature, user, default_enabled: true) - get_permission(user, access_level(feature)) + get_permission(user, feature) end def access_level(feature) @@ -134,12 +134,12 @@ class ProjectFeature < ActiveRecord::Base (FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")} end - def get_permission(user, level) - case level + def get_permission(user, feature) + case access_level(feature) when DISABLED false when PRIVATE - user && (project.team.member?(user) || user.full_private_access?) + team_access?(user, feature) when ENABLED true when PUBLIC @@ -148,4 +148,11 @@ class ProjectFeature < ActiveRecord::Base true end end + + def team_access?(user, feature) + return unless user + return true if user.full_private_access? + + project.team.member?(user, ProjectFeature.required_minimum_access_level(feature)) + end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 031b72ad9c3..87749ecf6c0 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -465,7 +465,7 @@ class ProjectPolicy < BasePolicy when ProjectFeature::DISABLED false when ProjectFeature::PRIVATE - guest? || admin? + admin? || team_access_level >= ProjectFeature.required_minimum_access_level(feature) else true end diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 40b8374848e..e75f0a184ea 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -32,7 +32,7 @@ = milestone_progress_bar(milestone) = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path · - = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path + = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path .float-lg-right.light #{milestone.percent_complete(current_user)}% complete .col-sm-2 .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 3b435847172..b877f66c71e 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -12,7 +12,7 @@ %li.nav-item = link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests - %span.badge.badge-pill= milestone.merge_requests.size + %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size - else %li.nav-item = link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do diff --git a/changelogs/unreleased/security-id-fix-mr-visibility.yml b/changelogs/unreleased/security-id-fix-mr-visibility.yml new file mode 100644 index 00000000000..8f41d191acc --- /dev/null +++ b/changelogs/unreleased/security-id-fix-mr-visibility.yml @@ -0,0 +1,5 @@ +--- +title: Display the correct number of MRs a user has access to +merge_request: +author: +type: security diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 79f854cdb96..503b88fcbad 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -13,269 +13,313 @@ describe MergeRequestsFinder do end end - let(:user) { create :user } - let(:user2) { create :user } - - let(:group) { create(:group) } - let(:subgroup) { create(:group, parent: group) } - let(:project1) { create_project_without_n_plus_1(group: group) } - let(:project2) do - Gitlab::GitalyClient.allow_n_plus_1_calls do - fork_project(project1, user) + context "multiple projects with merge requests" do + let(:user) { create :user } + let(:user2) { create :user } + + let(:group) { create(:group) } + let(:subgroup) { create(:group, parent: group) } + let(:project1) { create_project_without_n_plus_1(group: group) } + let(:project2) do + Gitlab::GitalyClient.allow_n_plus_1_calls do + fork_project(project1, user) + end end - end - let(:project3) do - Gitlab::GitalyClient.allow_n_plus_1_calls do - p = fork_project(project1, user) - p.update!(archived: true) - p + let(:project3) do + Gitlab::GitalyClient.allow_n_plus_1_calls do + p = fork_project(project1, user) + p.update!(archived: true) + p + end end - end - let(:project4) { create_project_without_n_plus_1(:repository, group: subgroup) } - let(:project5) { create_project_without_n_plus_1(group: subgroup) } - let(:project6) { create_project_without_n_plus_1(group: subgroup) } - - let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } - let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } - let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } - let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } - let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') } - let!(:merge_request6) { create(:merge_request, :simple, author: user, source_project: project5, target_project: project5, title: 'WIP: thing') } - let!(:merge_request7) { create(:merge_request, :simple, author: user, source_project: project6, target_project: project6, title: 'wip thing') } - let!(:merge_request8) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project1, title: '[wip] thing') } - let!(:merge_request9) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project2, title: 'wip: thing') } - - before do - project1.add_maintainer(user) - project2.add_developer(user) - project3.add_developer(user) - project2.add_developer(user2) - project4.add_developer(user) - project5.add_developer(user) - project6.add_developer(user) - end - - describe "#execute" do - it 'filters by scope' do - params = { scope: 'authored', state: 'opened' } - merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(7) + let(:project4) { create_project_without_n_plus_1(:repository, group: subgroup) } + let(:project5) { create_project_without_n_plus_1(group: subgroup) } + let(:project6) { create_project_without_n_plus_1(group: subgroup) } + + let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } + let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } + let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } + let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } + let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') } + let!(:merge_request6) { create(:merge_request, :simple, author: user, source_project: project5, target_project: project5, title: 'WIP: thing') } + let!(:merge_request7) { create(:merge_request, :simple, author: user, source_project: project6, target_project: project6, title: 'wip thing') } + let!(:merge_request8) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project1, title: '[wip] thing') } + let!(:merge_request9) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project2, title: 'wip: thing') } + + before do + project1.add_maintainer(user) + project2.add_developer(user) + project3.add_developer(user) + project2.add_developer(user2) + project4.add_developer(user) + project5.add_developer(user) + project6.add_developer(user) end - it 'filters by project' do - params = { project_id: project1.id, scope: 'authored', state: 'opened' } - merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(2) - end + describe '#execute' do + it 'filters by scope' do + params = { scope: 'authored', state: 'opened' } + merge_requests = described_class.new(user, params).execute + expect(merge_requests.size).to eq(7) + end - it 'filters by commit sha' do - merge_requests = described_class.new( - user, - commit_sha: merge_request5.merge_request_diff.last_commit_sha - ).execute + it 'filters by project' do + params = { project_id: project1.id, scope: 'authored', state: 'opened' } + merge_requests = described_class.new(user, params).execute + expect(merge_requests.size).to eq(2) + end - expect(merge_requests).to contain_exactly(merge_request5) - end + it 'filters by commit sha' do + merge_requests = described_class.new( + user, + commit_sha: merge_request5.merge_request_diff.last_commit_sha + ).execute + + expect(merge_requests).to contain_exactly(merge_request5) + end + + context 'filtering by group' do + it 'includes all merge requests when user has access' do + params = { group_id: group.id } + + merge_requests = described_class.new(user, params).execute - context 'filtering by group' do - it 'includes all merge requests when user has access' do - params = { group_id: group.id } + expect(merge_requests.size).to eq(3) + end + + it 'excludes merge requests from projects the user does not have access to' do + private_project = create_project_without_n_plus_1(:private, group: group) + private_mr = create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project) + params = { group_id: group.id } + + private_project.add_guest(user) + merge_requests = described_class.new(user, params).execute + + expect(merge_requests.size).to eq(3) + expect(merge_requests).not_to include(private_mr) + end + + it 'filters by group including subgroups', :nested_groups do + params = { group_id: group.id, include_subgroups: true } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests.size).to eq(6) + end + end + + it 'filters by non_archived' do + params = { non_archived: true } + merge_requests = described_class.new(user, params).execute + expect(merge_requests.size).to eq(8) + end + + it 'filters by iid' do + params = { project_id: project1.id, iids: merge_request1.iid } merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(3) + expect(merge_requests).to contain_exactly(merge_request1) end - it 'excludes merge requests from projects the user does not have access to' do - private_project = create_project_without_n_plus_1(:private, group: group) - private_mr = create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project) - params = { group_id: group.id } + it 'filters by source branch' do + params = { source_branch: merge_request2.source_branch } - private_project.add_guest(user) merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(3) - expect(merge_requests).not_to include(private_mr) + expect(merge_requests).to contain_exactly(merge_request2) end - it 'filters by group including subgroups', :nested_groups do - params = { group_id: group.id, include_subgroups: true } + it 'filters by target branch' do + params = { target_branch: merge_request2.target_branch } merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(6) + expect(merge_requests).to contain_exactly(merge_request2) end - end - it 'filters by non_archived' do - params = { non_archived: true } - merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(8) - end + it 'filters by state' do + params = { state: 'locked' } - it 'filters by iid' do - params = { project_id: project1.id, iids: merge_request1.iid } + merge_requests = described_class.new(user, params).execute - merge_requests = described_class.new(user, params).execute + expect(merge_requests).to contain_exactly(merge_request3) + end - expect(merge_requests).to contain_exactly(merge_request1) - end + it 'filters by wip' do + params = { wip: 'yes' } - it 'filters by source branch' do - params = { source_branch: merge_request2.source_branch } + merge_requests = described_class.new(user, params).execute - merge_requests = described_class.new(user, params).execute + expect(merge_requests).to contain_exactly(merge_request4, merge_request5, merge_request6, merge_request7, merge_request8, merge_request9) + end - expect(merge_requests).to contain_exactly(merge_request2) - end + it 'filters by not wip' do + params = { wip: 'no' } - it 'filters by target branch' do - params = { target_branch: merge_request2.target_branch } + merge_requests = described_class.new(user, params).execute - merge_requests = described_class.new(user, params).execute + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3) + end - expect(merge_requests).to contain_exactly(merge_request2) - end + it 'returns all items if no valid wip param exists' do + params = { wip: '' } - it 'filters by state' do - params = { state: 'locked' } + merge_requests = described_class.new(user, params).execute - merge_requests = described_class.new(user, params).execute + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5, merge_request6, merge_request7, merge_request8, merge_request9) + end - expect(merge_requests).to contain_exactly(merge_request3) - end + it 'adds wip to scalar params' do + scalar_params = described_class.scalar_params - it 'filters by wip' do - params = { wip: 'yes' } + expect(scalar_params).to include(:wip, :assignee_id) + end - merge_requests = described_class.new(user, params).execute + context 'filtering by group milestone' do + let!(:group) { create(:group, :public) } + let(:group_milestone) { create(:milestone, group: group) } + let!(:group_member) { create(:group_member, group: group, user: user) } + let(:params) { { milestone_title: group_milestone.title } } - expect(merge_requests).to contain_exactly(merge_request4, merge_request5, merge_request6, merge_request7, merge_request8, merge_request9) - end + before do + project2.update(namespace: group) + merge_request2.update(milestone: group_milestone) + merge_request3.update(milestone: group_milestone) + end - it 'filters by not wip' do - params = { wip: 'no' } + it 'returns issues assigned to that group milestone' do + merge_requests = described_class.new(user, params).execute - merge_requests = described_class.new(user, params).execute + expect(merge_requests).to contain_exactly(merge_request2, merge_request3) + end + end - expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3) - end + context 'filtering by created_at/updated_at' do + let(:new_project) { create(:project, forked_from_project: project1) } - it 'returns all items if no valid wip param exists' do - params = { wip: '' } + let!(:new_merge_request) do + create(:merge_request, + :simple, + author: user, + created_at: 1.week.from_now, + updated_at: 1.week.from_now, + source_project: new_project, + target_project: new_project) + end - merge_requests = described_class.new(user, params).execute + let!(:old_merge_request) do + create(:merge_request, + :simple, + author: user, + source_branch: 'feature_1', + created_at: 1.week.ago, + updated_at: 1.week.ago, + source_project: new_project, + target_project: new_project) + end - expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5, merge_request6, merge_request7, merge_request8, merge_request9) - end + before do + new_project.add_maintainer(user) + end - it 'adds wip to scalar params' do - scalar_params = described_class.scalar_params + it 'filters by created_after' do + params = { project_id: new_project.id, created_after: new_merge_request.created_at } - expect(scalar_params).to include(:wip, :assignee_id) - end + merge_requests = described_class.new(user, params).execute - context 'filtering by group milestone' do - let!(:group) { create(:group, :public) } - let(:group_milestone) { create(:milestone, group: group) } - let!(:group_member) { create(:group_member, group: group, user: user) } - let(:params) { { milestone_title: group_milestone.title } } + expect(merge_requests).to contain_exactly(new_merge_request) + end - before do - project2.update(namespace: group) - merge_request2.update(milestone: group_milestone) - merge_request3.update(milestone: group_milestone) - end + it 'filters by created_before' do + params = { project_id: new_project.id, created_before: old_merge_request.created_at } - it 'returns issues assigned to that group milestone' do - merge_requests = described_class.new(user, params).execute + merge_requests = described_class.new(user, params).execute - expect(merge_requests).to contain_exactly(merge_request2, merge_request3) - end - end + expect(merge_requests).to contain_exactly(old_merge_request) + end - context 'filtering by created_at/updated_at' do - let(:new_project) { create(:project, forked_from_project: project1) } - - let!(:new_merge_request) do - create(:merge_request, - :simple, - author: user, - created_at: 1.week.from_now, - updated_at: 1.week.from_now, - source_project: new_project, - target_project: new_project) - end + it 'filters by created_after and created_before' do + params = { + project_id: new_project.id, + created_after: old_merge_request.created_at, + created_before: new_merge_request.created_at + } - let!(:old_merge_request) do - create(:merge_request, - :simple, - author: user, - source_branch: 'feature_1', - created_at: 1.week.ago, - updated_at: 1.week.ago, - source_project: new_project, - target_project: new_project) - end + merge_requests = described_class.new(user, params).execute - before do - new_project.add_maintainer(user) - end + expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) + end - it 'filters by created_after' do - params = { project_id: new_project.id, created_after: new_merge_request.created_at } + it 'filters by updated_after' do + params = { project_id: new_project.id, updated_after: new_merge_request.updated_at } - merge_requests = described_class.new(user, params).execute + merge_requests = described_class.new(user, params).execute - expect(merge_requests).to contain_exactly(new_merge_request) - end + expect(merge_requests).to contain_exactly(new_merge_request) + end - it 'filters by created_before' do - params = { project_id: new_project.id, created_before: old_merge_request.created_at } + it 'filters by updated_before' do + params = { project_id: new_project.id, updated_before: old_merge_request.updated_at } - merge_requests = described_class.new(user, params).execute + merge_requests = described_class.new(user, params).execute - expect(merge_requests).to contain_exactly(old_merge_request) - end + expect(merge_requests).to contain_exactly(old_merge_request) + end - it 'filters by created_after and created_before' do - params = { - project_id: new_project.id, - created_after: old_merge_request.created_at, - created_before: new_merge_request.created_at - } + it 'filters by updated_after and updated_before' do + params = { + project_id: new_project.id, + updated_after: old_merge_request.updated_at, + updated_before: new_merge_request.updated_at + } - merge_requests = described_class.new(user, params).execute + merge_requests = described_class.new(user, params).execute - expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) + expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) + end end + end - it 'filters by updated_after' do - params = { project_id: new_project.id, updated_after: new_merge_request.updated_at } + describe '#row_count', :request_store do + it 'returns the number of rows for the default state' do + finder = described_class.new(user) - merge_requests = described_class.new(user, params).execute + expect(finder.row_count).to eq(7) + end + + it 'returns the number of rows for a given state' do + finder = described_class.new(user, state: 'closed') - expect(merge_requests).to contain_exactly(new_merge_request) + expect(finder.row_count).to eq(1) end + end + end - it 'filters by updated_before' do - params = { project_id: new_project.id, updated_before: old_merge_request.updated_at } + context 'when projects require different access levels for merge requests' do + let(:user) { create(:user) } - merge_requests = described_class.new(user, params).execute + let(:public_project) { create(:project, :public) } + let(:internal) { create(:project, :internal) } + let(:private_project) { create(:project, :private) } + let(:public_with_private_repo) { create(:project, :public, :repository, :repository_private) } + let(:internal_with_private_repo) { create(:project, :internal, :repository, :repository_private) } - expect(merge_requests).to contain_exactly(old_merge_request) - end + let(:merge_requests) { described_class.new(user, {}).execute } - it 'filters by updated_after and updated_before' do - params = { - project_id: new_project.id, - updated_after: old_merge_request.updated_at, - updated_before: new_merge_request.updated_at - } + let!(:mr_public) { create(:merge_request, source_project: public_project) } + let!(:mr_private) { create(:merge_request, source_project: private_project) } + let!(:mr_internal) { create(:merge_request, source_project: internal) } + let!(:mr_private_repo_access) { create(:merge_request, source_project: public_with_private_repo) } + let!(:mr_internal_private_repo_access) { create(:merge_request, source_project: internal_with_private_repo) } - merge_requests = described_class.new(user, params).execute + context 'with admin user' do + let(:user) { create(:user, :admin) } - expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) + it 'returns all merge requests' do + expect(merge_requests).to eq( + [mr_internal_private_repo_access, mr_private_repo_access, mr_internal, mr_private, mr_public] + ) end end @@ -293,19 +337,85 @@ describe MergeRequestsFinder do expect(merge_requests).to be_empty end end - end - describe '#row_count', :request_store do - it 'returns the number of rows for the default state' do - finder = described_class.new(user) + context 'with external user' do + let(:user) { create(:user, :external) } - expect(finder.row_count).to eq(7) + it 'returns only public merge requests' do + expect(merge_requests).to eq([mr_public]) + end end - it 'returns the number of rows for a given state' do - finder = described_class.new(user, state: 'closed') + context 'with authenticated user' do + it 'returns public and internal merge requests' do + expect(merge_requests).to eq([mr_internal, mr_public]) + end + + context 'being added to the private project' do + context 'as a guest' do + before do + private_project.add_guest(user) + end + + it 'does not return merge requests from the private project' do + expect(merge_requests).to eq([mr_internal, mr_public]) + end + end + + context 'as a developer' do + before do + private_project.add_developer(user) + end + + it 'returns merge requests from the private project' do + expect(merge_requests).to eq([mr_internal, mr_private, mr_public]) + end + end + end - expect(finder.row_count).to eq(1) + context 'being added to the public project with private repo access' do + context 'as a guest' do + before do + public_with_private_repo.add_guest(user) + end + + it 'returns merge requests from the project' do + expect(merge_requests).to eq([mr_internal, mr_public]) + end + end + + context 'as a reporter' do + before do + public_with_private_repo.add_reporter(user) + end + + it 'returns merge requests from the project' do + expect(merge_requests).to eq([mr_private_repo_access, mr_internal, mr_public]) + end + end + end + + context 'being added to the internal project with private repo access' do + context 'as a guest' do + before do + internal_with_private_repo.add_guest(user) + end + + it 'returns merge requests from the project' do + expect(merge_requests).to eq([mr_internal, mr_public]) + end + end + + context 'as a reporter' do + before do + internal_with_private_repo.add_reporter(user) + end + + it 'returns merge requests from the project' do + expect(merge_requests).to eq([mr_internal_private_repo_access, mr_internal, mr_public]) + end + end + end end end end -- GitLab