Commit 002ad215 authored by Yorick Peterse's avatar Yorick Peterse

Method for returning issues readable by a user

The method Ability.issues_readable_by_user takes a list of users and an
optional user and returns an Array of issues readable by said user. This
method in turn is used by
Banzai::ReferenceParser::IssueParser#nodes_visible_to_user so this
method no longer needs to get all the available abilities just to check
if a user has the "read_issue" ability.

To test this I benchmarked an issue with 222 comments on my development
environment. Using these changes the time spent in nodes_visible_to_user
was reduced from around 120 ms to around 40 ms.
parent 9b0e131b
...@@ -14,6 +14,7 @@ v 8.11.0 (unreleased) ...@@ -14,6 +14,7 @@ v 8.11.0 (unreleased)
- Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects
- Retrieve rendered HTML from cache in one request - Retrieve rendered HTML from cache in one request
- Fix renaming repository when name contains invalid chararacters under project settings - Fix renaming repository when name contains invalid chararacters under project settings
- Optimize checking if a user has read access to a list of issues !5370
- Nokogiri's various parsing methods are now instrumented - Nokogiri's various parsing methods are now instrumented
- Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363
- Add build event color in HipChat messages (David Eisner) - Add build event color in HipChat messages (David Eisner)
......
...@@ -47,6 +47,16 @@ class Ability ...@@ -47,6 +47,16 @@ class Ability
end end
end end
# Returns an Array of Issues that can be read by the given user.
#
# issues - The issues to reduce down to those readable by the user.
# user - The User for which to check the issues
def issues_readable_by_user(issues, user = nil)
return issues if user && user.admin?
issues.select { |issue| issue.visible_to_user?(user) }
end
# List of possible abilities for anonymous user # List of possible abilities for anonymous user
def anonymous_abilities(user, subject) def anonymous_abilities(user, subject)
if subject.is_a?(PersonalSnippet) if subject.is_a?(PersonalSnippet)
......
...@@ -230,6 +230,34 @@ class Issue < ActiveRecord::Base ...@@ -230,6 +230,34 @@ class Issue < ActiveRecord::Base
self.closed_by_merge_requests(current_user).empty? self.closed_by_merge_requests(current_user).empty?
end end
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
user ? readable_by?(user) : publicly_visible?
end
# Returns `true` if the given User can read the current Issue.
def readable_by?(user)
if user.admin?
true
elsif project.owner == user
true
elsif confidential?
author == user ||
assignee == user ||
project.team.member?(user, Gitlab::Access::REPORTER)
else
project.public? ||
project.internal? && !user.external? ||
project.team.member?(user)
end
end
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
project.public? && !confidential?
end
def overdue? def overdue?
due_date.try(:past?) || false due_date.try(:past?) || false
end end
......
...@@ -9,10 +9,11 @@ module Banzai ...@@ -9,10 +9,11 @@ module Banzai
issues = issues_for_nodes(nodes) issues = issues_for_nodes(nodes)
nodes.select do |node| readable_issues = Ability.
issue = issue_for_node(issues, node) issues_readable_by_user(issues.values, user).to_set
issue ? can?(user, :read_issue, issue) : false nodes.select do |node|
readable_issues.include?(issue_for_node(issues, node))
end end
end end
......
...@@ -16,17 +16,17 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do ...@@ -16,17 +16,17 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
end end
it 'returns the nodes when the user can read the issue' do it 'returns the nodes when the user can read the issue' do
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:issues_readable_by_user).
with(user, :read_issue, issue). with([issue], user).
and_return(true) and_return([issue])
expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end end
it 'returns an empty Array when the user can not read the issue' do it 'returns an empty Array when the user can not read the issue' do
expect(Ability.abilities).to receive(:allowed?). expect(Ability).to receive(:issues_readable_by_user).
with(user, :read_issue, issue). with([issue], user).
and_return(false) and_return([])
expect(subject.nodes_visible_to_user(user, [link])).to eq([]) expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end end
......
...@@ -170,4 +170,52 @@ describe Ability, lib: true do ...@@ -170,4 +170,52 @@ describe Ability, lib: true do
end end
end end
end end
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
user = build(:user, admin: true)
issue = build(:issue)
expect(described_class.issues_readable_by_user([issue], user)).
to eq([issue])
end
end
context 'with a regular user' do
it 'returns the issues readable by the user' do
user = build(:user)
issue = build(:issue)
expect(issue).to receive(:readable_by?).with(user).and_return(true)
expect(described_class.issues_readable_by_user([issue], user)).
to eq([issue])
end
it 'returns an empty Array when no issues are readable' do
user = build(:user)
issue = build(:issue)
expect(issue).to receive(:readable_by?).with(user).and_return(false)
expect(described_class.issues_readable_by_user([issue], user)).to eq([])
end
end
context 'without a regular user' do
it 'returns issues that are publicly visible' do
hidden_issue = build(:issue)
visible_issue = build(:issue)
expect(hidden_issue).to receive(:publicly_visible?).and_return(false)
expect(visible_issue).to receive(:publicly_visible?).and_return(true)
issues = described_class.
issues_readable_by_user([hidden_issue, visible_issue])
expect(issues).to eq([visible_issue])
end
end
end
end end
...@@ -68,7 +68,7 @@ describe Issue, "Mentionable" do ...@@ -68,7 +68,7 @@ describe Issue, "Mentionable" do
describe '#create_cross_references!' do describe '#create_cross_references!' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:author) { double('author') } let(:author) { build(:user) }
let(:commit) { project.commit } let(:commit) { project.commit }
let(:commit2) { project.commit } let(:commit2) { project.commit }
...@@ -88,6 +88,10 @@ describe Issue, "Mentionable" do ...@@ -88,6 +88,10 @@ describe Issue, "Mentionable" do
let(:author) { create(:author) } let(:author) { create(:author) }
let(:issues) { create_list(:issue, 2, project: project, author: author) } let(:issues) { create_list(:issue, 2, project: project, author: author) }
before do
project.team << [author, Gitlab::Access::DEVELOPER]
end
context 'before changes are persisted' do context 'before changes are persisted' do
it 'ignores pre-existing references' do it 'ignores pre-existing references' do
issue = create_issue(description: issues[0].to_reference) issue = create_issue(description: issues[0].to_reference)
......
...@@ -306,4 +306,257 @@ describe Issue, models: true do ...@@ -306,4 +306,257 @@ describe Issue, models: true do
expect(user2.assigned_open_issues_count).to eq(1) expect(user2.assigned_open_issues_count).to eq(1)
end end
end end
describe '#visible_to_user?' do
context 'with a user' do
let(:user) { build(:user) }
let(:issue) { build(:issue) }
it 'returns true when the issue is readable' do
expect(issue).to receive(:readable_by?).with(user).and_return(true)
expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns false when the issue is not readable' do
expect(issue).to receive(:readable_by?).with(user).and_return(false)
expect(issue.visible_to_user?(user)).to eq(false)
end
end
context 'without a user' do
let(:issue) { build(:issue) }
it 'returns true when the issue is publicly visible' do
expect(issue).to receive(:publicly_visible?).and_return(true)
expect(issue.visible_to_user?).to eq(true)
end
it 'returns false when the issue is not publicly visible' do
expect(issue).to receive(:publicly_visible?).and_return(false)
expect(issue.visible_to_user?).to eq(false)
end
end
end
describe '#readable_by?' do
describe 'with a regular user that is not a team member' do
let(:user) { create(:user) }
context 'using a public project' do
let(:project) { create(:empty_project, :public) }
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
end
it 'returns false for a confidential issue' do
issue = build(:issue, project: project, confidential: true)
expect(issue).not_to be_readable_by(user)
end
end
context 'using an internal project' do
let(:project) { create(:empty_project, :internal) }
context 'using an internal user' do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_readable_by(user)
end
end
context 'using an external user' do
before do
allow(user).to receive(:external?).and_return(true)
end
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_readable_by(user)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_readable_by(user)
end
end
end
context 'using a private project' do
let(:project) { create(:empty_project, :private) }
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_readable_by(user)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_readable_by(user)
end
context 'when the user is the project owner' do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_readable_by(user)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_readable_by(user)
end
end
end
end
context 'with a regular user that is a team member' do
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
context 'using a public project' do
before do
project.team << [user, Gitlab::Access::DEVELOPER]
end
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).to be_readable_by(user)
end
end
context 'using an internal project' do
let(:project) { create(:empty_project, :internal) }
before do
project.team << [user, Gitlab::Access::DEVELOPER]
end
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).to be_readable_by(user)
end
end
context 'using a private project' do
let(:project) { create(:empty_project, :private) }
before do
project.team << [user, Gitlab::Access::DEVELOPER]
end
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).to be_readable_by(user)
end
end
end
context 'with an admin user' do
let(:project) { create(:empty_project) }
let(:user) { create(:user, admin: true) }
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).to be_readable_by(user)
end
end
end
describe '#publicly_visible?' do
context 'using a public project' do
let(:project) { create(:empty_project, :public) }
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_publicly_visible
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_publicly_visible
end
end
context 'using an internal project' do
let(:project) { create(:empty_project, :internal) }
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_publicly_visible
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_publicly_visible
end
end
context 'using a private project' do
let(:project) { create(:empty_project, :private) }
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_publicly_visible
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_publicly_visible
end
end
end
end end
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