Skip to content

Commit 369d34c

Browse files
author
Yorick Peterse
committed
Merge branch '42877-fix-visibility-change-performance' into 'master'
Revert Project.public_or_visible_to_user changes but apply change to SnippetsFinder Closes #42877 See merge request gitlab-org/gitlab-ce!17476
2 parents ddad22c + 39011be commit 369d34c

File tree

5 files changed

+74
-40
lines changed

5 files changed

+74
-40
lines changed

app/finders/snippets_finder.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,37 @@ def authorized_snippets
5858
.public_or_visible_to_user(current_user)
5959
end
6060

61+
# Returns a collection of projects that is either public or visible to the
62+
# logged in user.
63+
#
64+
# A caller must pass in a block to modify individual parts of
65+
# the query, e.g. to apply .with_feature_available_for_user on top of it.
66+
# This is useful for performance as we can stick those additional filters
67+
# at the bottom of e.g. the UNION.
68+
def projects_for_user
69+
return yield(Project.public_to_user) unless current_user
70+
71+
# If the current_user is allowed to see all projects,
72+
# we can shortcut and just return.
73+
return yield(Project.all) if current_user.full_private_access?
74+
75+
authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))
76+
77+
levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
78+
visible_projects = yield(Project.where(visibility_level: levels))
79+
80+
# We use a UNION here instead of OR clauses since this results in better
81+
# performance.
82+
union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
83+
84+
Project.from("(#{union.to_sql}) AS #{Project.table_name}")
85+
end
86+
6187
def feature_available_projects
6288
# Don't return any project related snippets if the user cannot read cross project
6389
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
6490

65-
projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part|
91+
projects = projects_for_user do |part|
6692
part.with_feature_available_for_user(:snippets, current_user)
6793
end.select(:id)
6894

app/models/project.rb

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -317,42 +317,13 @@ class Project < ActiveRecord::Base
317317

318318
# Returns a collection of projects that is either public or visible to the
319319
# logged in user.
320-
#
321-
# A caller may pass in a block to modify individual parts of
322-
# the query, e.g. to apply .with_feature_available_for_user on top of it.
323-
# This is useful for performance as we can stick those additional filters
324-
# at the bottom of e.g. the UNION.
325-
#
326-
# Optionally, turning `use_where_in` off leads to returning a
327-
# relation using #from instead of #where. This can perform much better
328-
# but leads to trouble when used in conjunction with AR's #merge method.
329-
def self.public_or_visible_to_user(user = nil, use_where_in: true, &block)
330-
# If we don't get a block passed, use identity to avoid if/else repetitions
331-
block = ->(part) { part } unless block_given?
332-
333-
return block.call(public_to_user) unless user
334-
335-
# If the user is allowed to see all projects,
336-
# we can shortcut and just return.
337-
return block.call(all) if user.full_private_access?
338-
339-
authorized = user
340-
.project_authorizations
341-
.select(1)
342-
.where('project_authorizations.project_id = projects.id')
343-
authorized_projects = block.call(where('EXISTS (?)', authorized))
344-
345-
levels = Gitlab::VisibilityLevel.levels_for_user(user)
346-
visible_projects = block.call(where(visibility_level: levels))
347-
348-
# We use a UNION here instead of OR clauses since this results in better
349-
# performance.
350-
union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
351-
352-
if use_where_in
353-
where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
320+
def self.public_or_visible_to_user(user = nil)
321+
if user
322+
where('EXISTS (?) OR projects.visibility_level IN (?)',
323+
user.authorizations_for_projects,
324+
Gitlab::VisibilityLevel.levels_for_user(user))
354325
else
355-
from("(#{union.to_sql}) AS #{table_name}")
326+
public_to_user
356327
end
357328
end
358329

@@ -371,14 +342,11 @@ def self.with_feature_available_for_user(feature, user)
371342
elsif user
372343
column = ProjectFeature.quoted_access_level_column(feature)
373344

374-
authorized = user.project_authorizations.select(1)
375-
.where('project_authorizations.project_id = projects.id')
376-
377345
with_project_feature
378346
.where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
379347
visible,
380348
ProjectFeature::PRIVATE,
381-
authorized)
349+
user.authorizations_for_projects)
382350
else
383351
with_feature_access_level(feature, visible)
384352
end

app/models/user.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,15 @@ def authorized_project?(project, min_access_level = nil)
601601
authorized_projects(min_access_level).exists?({ id: project.id })
602602
end
603603

604+
# Typically used in conjunction with projects table to get projects
605+
# a user has been given access to.
606+
#
607+
# Example use:
608+
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
609+
def authorizations_for_projects
610+
project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
611+
end
612+
604613
# Returns the projects this user has reporter (or greater) access to, limited
605614
# to at most the given projects.
606615
#
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: Revert Project.public_or_visible_to_user changes and only apply to snippets
3+
merge_request:
4+
author:
5+
type: fixed

spec/models/user_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,32 @@
16351635
end
16361636
end
16371637

1638+
describe '#authorizations_for_projects' do
1639+
let!(:user) { create(:user) }
1640+
subject { Project.where("EXISTS (?)", user.authorizations_for_projects) }
1641+
1642+
it 'includes projects that belong to a user, but no other projects' do
1643+
owned = create(:project, :private, namespace: user.namespace)
1644+
member = create(:project, :private).tap { |p| p.add_master(user) }
1645+
other = create(:project)
1646+
1647+
expect(subject).to include(owned)
1648+
expect(subject).to include(member)
1649+
expect(subject).not_to include(other)
1650+
end
1651+
1652+
it 'includes projects a user has access to, but no other projects' do
1653+
other_user = create(:user)
1654+
accessible = create(:project, :private, namespace: other_user.namespace) do |project|
1655+
project.add_developer(user)
1656+
end
1657+
other = create(:project)
1658+
1659+
expect(subject).to include(accessible)
1660+
expect(subject).not_to include(other)
1661+
end
1662+
end
1663+
16381664
describe '#authorized_projects', :delete do
16391665
context 'with a minimum access level' do
16401666
it 'includes projects for which the user is an owner' do

0 commit comments

Comments
 (0)