Commit 1a05b196 authored by Douwe Maan's avatar Douwe Maan Committed by Alejandro Rodriguez

Merge branch 'jej-use-issuable-finder-instead-of-access-check' into 'security'

Replace issue access checks with use of IssuableFinder

Split from !2024 to partially solve https://gitlab.com/gitlab-org/gitlab-ce/issues/23867

## Which fixes are in this MR?

 - Potentially untested  
💣 - No test coverage  
🚥 - Test coverage of some sort exists (a test failed when error raised)  
🚦 - Test coverage of return value (a test failed when nil used)  
 - Permissions check tested

### Issue lookup with access check

Using `visible_to_user` likely makes these security issues too. See [Code smells](#code-smells).

- [x] 🚦 app/finders/notes_finder.rb:15 [`visible_to_user`]
- [x] 🚥 app/views/layouts/nav/_project.html.haml:73 [`visible_to_user`] [`.count`]
- [x]  app/services/merge_requests/build_service.rb:84 [`issue.try(:confidential?)`]
- [x]  lib/api/issues.rb:112 [`visible_to_user`]
  - CHANGELOG: Prevented API returning issues set to 'Only team members' to everyone
- [x]  lib/api/helpers.rb:126 [`can?(current_user, :read_issue, issue)`] Maybe here too?
- [x]  lib/gitlab/search_results.rb:53 [`visible_to_user`]

### Previous discussions
- [ ] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#b2ff264eddf9819d7693c14ae213d941494fe2b3_128_126
- [ ] https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2024/diffs#7b6375270d22f880bdcb085e47b519b426a5c6c7_87_87

See merge request !2031
parent d835f50b
...@@ -23,7 +23,7 @@ class IssuableFinder ...@@ -23,7 +23,7 @@ class IssuableFinder
attr_accessor :current_user, :params attr_accessor :current_user, :params
def initialize(current_user, params) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@params = params @params = params
end end
......
...@@ -12,7 +12,7 @@ class NotesFinder ...@@ -12,7 +12,7 @@ class NotesFinder
when "commit" when "commit"
project.notes.for_commit_id(target_id).non_diff_notes project.notes.for_commit_id(target_id).non_diff_notes
when "issue" when "issue"
project.issues.visible_to_user(current_user).find(target_id).notes.inc_author IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
......
...@@ -683,9 +683,9 @@ class Project < ActiveRecord::Base ...@@ -683,9 +683,9 @@ class Project < ActiveRecord::Base
self.id self.id
end end
def get_issue(issue_id) def get_issue(issue_id, current_user)
if default_issues_tracker? if default_issues_tracker?
issues.find_by(iid: issue_id) IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id)
else else
ExternalIssue.new(issue_id, self) ExternalIssue.new(issue_id, self)
end end
......
...@@ -81,7 +81,7 @@ module MergeRequests ...@@ -81,7 +81,7 @@ module MergeRequests
commit = commits.first commit = commits.first
merge_request.title = commit.title merge_request.title = commit.title
merge_request.description ||= commit.description.try(:strip) merge_request.description ||= commit.description.try(:strip)
elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) elsif iid && issue = merge_request.target_project.get_issue(iid, current_user)
case issue case issue
when Issue when Issue
merge_request.title = "Resolve \"#{issue.title}\"" merge_request.title = "Resolve \"#{issue.title}\""
......
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
%span %span
Issues Issues
- if @project.default_issues_tracker? - if @project.default_issues_tracker?
%span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :merge_requests - if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do = nav_link(controller: :merge_requests) do
......
---
title: Replace issue access checks with use of IssuableFinder
merge_request:
author:
...@@ -112,9 +112,7 @@ module API ...@@ -112,9 +112,7 @@ module API
end end
def find_project_issue(id) def find_project_issue(id)
issue = user_project.issues.find(id) IssuesFinder.new(current_user, project_id: user_project.id).find(id)
not_found! unless can?(current_user, :read_issue, issue)
issue
end end
def paginate(relation) def paginate(relation)
......
...@@ -122,7 +122,7 @@ module API ...@@ -122,7 +122,7 @@ module API
# GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /projects/:id/issues?milestone=1.0.0&state=closed
# GET /issues?iid=42 # GET /issues?iid=42
get ":id/issues" do get ":id/issues" do
issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user) issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
......
...@@ -50,7 +50,7 @@ module Gitlab ...@@ -50,7 +50,7 @@ module Gitlab
end end
def issues def issues
issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation) issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation)
if query =~ /#(\d+)\z/ if query =~ /#(\d+)\z/
issues = issues.where(iid: $1) issues = issues.where(iid: $1)
......
...@@ -65,6 +65,14 @@ describe Gitlab::ProjectSearchResults, lib: true do ...@@ -65,6 +65,14 @@ describe Gitlab::ProjectSearchResults, lib: true do
end end
end end
it 'does not list issues on private projects' do
issue = create(:issue, project: project)
results = described_class.new(user, project, issue.title)
expect(results.objects('issues')).not_to include issue
end
describe 'confidential issues' do describe 'confidential issues' do
let(:query) { 'issue' } let(:query) { 'issue' }
let(:author) { create(:user) } let(:author) { create(:user) }
...@@ -72,6 +80,7 @@ describe Gitlab::ProjectSearchResults, lib: true do ...@@ -72,6 +80,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
let(:member) { create(:user) } let(:member) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:project) { create(:empty_project, :internal) }
let!(:issue) { create(:issue, project: project, title: 'Issue 1') } let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
......
...@@ -12,35 +12,48 @@ describe Gitlab::SearchResults do ...@@ -12,35 +12,48 @@ describe Gitlab::SearchResults do
let!(:milestone) { create(:milestone, project: project, title: 'foo') } let!(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:results) { described_class.new(user, Project.all, 'foo') } let(:results) { described_class.new(user, Project.all, 'foo') }
describe '#projects_count' do context 'as a user with access' do
it 'returns the total amount of projects' do before do
expect(results.projects_count).to eq(1) project.team << [user, :developer]
end end
end
describe '#issues_count' do describe '#projects_count' do
it 'returns the total amount of issues' do it 'returns the total amount of projects' do
expect(results.issues_count).to eq(1) expect(results.projects_count).to eq(1)
end
end end
end
describe '#merge_requests_count' do describe '#issues_count' do
it 'returns the total amount of merge requests' do it 'returns the total amount of issues' do
expect(results.merge_requests_count).to eq(1) expect(results.issues_count).to eq(1)
end
end
describe '#merge_requests_count' do
it 'returns the total amount of merge requests' do
expect(results.merge_requests_count).to eq(1)
end
end end
end
describe '#milestones_count' do describe '#milestones_count' do
it 'returns the total amount of milestones' do it 'returns the total amount of milestones' do
expect(results.milestones_count).to eq(1) expect(results.milestones_count).to eq(1)
end
end end
end end
it 'does not list issues on private projects' do
private_project = create(:empty_project, :private)
issue = create(:issue, project: private_project, title: 'foo')
expect(results.objects('issues')).not_to include issue
end
describe 'confidential issues' do describe 'confidential issues' do
let(:project_1) { create(:empty_project) } let(:project_1) { create(:empty_project, :internal) }
let(:project_2) { create(:empty_project) } let(:project_2) { create(:empty_project, :internal) }
let(:project_3) { create(:empty_project) } let(:project_3) { create(:empty_project, :internal) }
let(:project_4) { create(:empty_project) } let(:project_4) { create(:empty_project, :internal) }
let(:query) { 'issue' } let(:query) { 'issue' }
let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) } let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) }
let(:author) { create(:user) } let(:author) { create(:user) }
......
...@@ -354,10 +354,15 @@ describe Project, models: true do ...@@ -354,10 +354,15 @@ describe Project, models: true do
describe '#get_issue' do describe '#get_issue' do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let!(:issue) { create(:issue, project: project) } let!(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
before do
project.team << [user, :developer]
end
context 'with default issues tracker' do context 'with default issues tracker' do
it 'returns an issue' do it 'returns an issue' do
expect(project.get_issue(issue.iid)).to eq issue expect(project.get_issue(issue.iid, user)).to eq issue
end end
it 'returns count of open issues' do it 'returns count of open issues' do
...@@ -365,7 +370,12 @@ describe Project, models: true do ...@@ -365,7 +370,12 @@ describe Project, models: true do
end end
it 'returns nil when no issue found' do it 'returns nil when no issue found' do
expect(project.get_issue(999)).to be_nil expect(project.get_issue(999, user)).to be_nil
end
it "returns nil when user doesn't have access" do
user = create(:user)
expect(project.get_issue(issue.iid, user)).to eq nil
end end
end end
...@@ -375,7 +385,7 @@ describe Project, models: true do ...@@ -375,7 +385,7 @@ describe Project, models: true do
end end
it 'returns an ExternalIssue' do it 'returns an ExternalIssue' do
issue = project.get_issue('FOO-1234') issue = project.get_issue('FOO-1234', user)
expect(issue).to be_kind_of(ExternalIssue) expect(issue).to be_kind_of(ExternalIssue)
expect(issue.iid).to eq 'FOO-1234' expect(issue.iid).to eq 'FOO-1234'
expect(issue.project).to eq project expect(issue.project).to eq project
......
...@@ -365,6 +365,24 @@ describe API::API, api: true do ...@@ -365,6 +365,24 @@ describe API::API, api: true do
let(:base_url) { "/projects/#{project.id}" } let(:base_url) { "/projects/#{project.id}" }
let(:title) { milestone.title } let(:title) { milestone.title }
it "returns 404 on private projects for other users" do
private_project = create(:empty_project, :private)
create(:issue, project: private_project)
get api("/projects/#{private_project.id}/issues", non_member)
expect(response).to have_http_status(404)
end
it 'returns no issues when user has access to project but not issues' do
restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
create(:issue, project: restricted_project)
get api("/projects/#{restricted_project.id}/issues", non_member)
expect(json_response).to eq([])
end
it 'returns project issues without confidential issues for non project members' do it 'returns project issues without confidential issues for non project members' do
get api("#{base_url}/issues", non_member) get api("#{base_url}/issues", non_member)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
......
...@@ -24,6 +24,8 @@ describe MergeRequests::BuildService, services: true do ...@@ -24,6 +24,8 @@ describe MergeRequests::BuildService, services: true do
end end
before do before do
project.team << [user, :guest]
allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare)
allow(project).to receive(:commit).and_return(commit_1) allow(project).to receive(:commit).and_return(commit_1)
allow(project).to receive(:commit).and_return(commit_2) allow(project).to receive(:commit).and_return(commit_2)
...@@ -168,6 +170,16 @@ describe MergeRequests::BuildService, services: true do ...@@ -168,6 +170,16 @@ describe MergeRequests::BuildService, services: true do
expect(merge_request.title).to eq("Resolve \"#{issue.title}\"") expect(merge_request.title).to eq("Resolve \"#{issue.title}\"")
end end
context 'when issue is not accessible to user' do
before do
project.team.truncate
end
it 'uses branch title as the merge request title' do
expect(merge_request.title).to eq("#{issue.iid} fix issue")
end
end
context 'issue does not exist' do context 'issue does not exist' do
let(:source_branch) { "#{issue.iid.succ}-fix-issue" } let(:source_branch) { "#{issue.iid.succ}-fix-issue" }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment