Commit 0a396dda authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'tc-refactor-projects-finder-init-collection-ee' into 'master'

CE to EE: refactor projects finder init collection ee

See merge request !2199
parents 1d0701cb dc062ccb
...@@ -33,7 +33,8 @@ class EventsFinder ...@@ -33,7 +33,8 @@ class EventsFinder
private private
def by_current_user_access(events) def by_current_user_access(events)
events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project) events.merge(ProjectsFinder.new(current_user: current_user).execute).
joins(:project)
end end
def by_action(events) def by_action(events)
......
...@@ -29,35 +29,69 @@ class GroupProjectsFinder < ProjectsFinder ...@@ -29,35 +29,69 @@ class GroupProjectsFinder < ProjectsFinder
private private
def init_collection def init_collection
only_owned = options.fetch(:only_owned, false) projects = if current_user
only_shared = options.fetch(:only_shared, false) collection_with_user
else
collection_without_user
end
projects = [] union(projects)
end
if current_user def collection_with_user
if group.users.include?(current_user) if group.users.include?(current_user)
projects << group.projects unless only_shared if only_shared?
projects << group.shared_projects unless only_owned [shared_projects]
elsif only_owned?
[owned_projects]
else else
unless only_shared [shared_projects, owned_projects]
projects << group.projects.visible_to_user(current_user)
projects << group.projects.public_to_user(current_user)
end
unless only_owned
projects << group.shared_projects.visible_to_user(current_user)
projects << group.shared_projects.public_to_user(current_user)
end
end end
else else
projects << group.projects.public_only unless only_shared if only_shared?
projects << group.shared_projects.public_only unless only_owned [shared_projects.public_or_visible_to_user(current_user)]
elsif only_owned?
[owned_projects.public_or_visible_to_user(current_user)]
else
[
owned_projects.public_or_visible_to_user(current_user),
shared_projects.public_or_visible_to_user(current_user)
]
end
end end
end
projects def collection_without_user
if only_shared?
[shared_projects.public_only]
elsif only_owned?
[owned_projects.public_only]
else
[shared_projects.public_only, owned_projects.public_only]
end
end end
def union(items) def union(items)
find_union(items, Project) if items.one?
items.first
else
find_union(items, Project)
end
end
def only_owned?
options.fetch(:only_owned, false)
end
def only_shared?
options.fetch(:only_shared, false)
end
def owned_projects
group.projects
end
def shared_projects
group.shared_projects
end end
end end
...@@ -28,34 +28,56 @@ class ProjectsFinder < UnionFinder ...@@ -28,34 +28,56 @@ class ProjectsFinder < UnionFinder
end end
def execute def execute
items = init_collection collection = init_collection
items = items.map do |item| collection = by_ids(collection)
item = by_ids(item) collection = by_personal(collection)
item = by_personal(item) collection = by_starred(collection)
item = by_starred(item) collection = by_trending(collection)
item = by_trending(item) collection = by_visibilty_level(collection)
item = by_visibilty_level(item) collection = by_tags(collection)
item = by_tags(item) collection = by_search(collection)
item = by_search(item) collection = by_archived(collection)
by_archived(item)
end sort(collection)
items = union(items)
sort(items)
end end
private private
def init_collection def init_collection
projects = [] if current_user
collection_with_user
else
collection_without_user
end
end
if params[:owned].present? def collection_with_user
projects << current_user.owned_projects if current_user if owned_projects?
current_user.owned_projects
else else
projects << current_user.authorized_projects if current_user if private_only?
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present? current_user.authorized_projects
else
Project.public_or_visible_to_user(current_user)
end
end end
end
# Builds a collection for an anonymous user.
def collection_without_user
if private_only? || owned_projects?
Project.none
else
Project.public_to_user
end
end
def owned_projects?
params[:owned].present?
end
projects def private_only?
params[:non_public].present?
end end
def by_ids(items) def by_ids(items)
......
...@@ -56,6 +56,27 @@ module EE ...@@ -56,6 +56,27 @@ module EE
admin? || auditor? admin? || auditor?
end end
def access_level
if auditor?
:auditor
else
super
end
end
def access_level=(new_level)
new_level = new_level.to_s
return unless %w(admin auditor regular).include?(new_level)
self.admin = (new_level == 'admin')
self.auditor = (new_level == 'auditor')
end
# Does the user have access to all private groups & projects?
def has_full_private_access?
admin_or_auditor?
end
def remember_me! def remember_me!
return if ::Gitlab::Geo.secondary? return if ::Gitlab::Geo.secondary?
super super
......
...@@ -271,20 +271,49 @@ class Project < ActiveRecord::Base ...@@ -271,20 +271,49 @@ class Project < ActiveRecord::Base
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
# Returns a collection of projects that is either public or visible to the
# logged in user.
def self.public_or_visible_to_user(user = nil)
if user
authorized = user.
project_authorizations.
select(1).
where('project_authorizations.project_id = projects.id')
levels = Gitlab::VisibilityLevel.levels_for_user(user)
where('EXISTS (?) OR projects.visibility_level IN (?)', authorized, levels)
else
public_to_user
end
end
# project features may be "disabled", "internal" or "enabled". If "internal", # project features may be "disabled", "internal" or "enabled". If "internal",
# they are only available to team members. This scope returns projects where # they are only available to team members. This scope returns projects where
# the feature is either enabled, or internal with permission for the user. # the feature is either enabled, or internal with permission for the user.
#
# This method uses an optimised version of `with_feature_access_level` for
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user) def self.with_feature_available_for_user(feature, user)
return with_feature_enabled(feature) if user.try(:admin?) visible = [nil, ProjectFeature::ENABLED]
unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED]) if user&.admin?
return unconditional if user.nil? with_feature_enabled(feature)
elsif user
column = ProjectFeature.quoted_access_level_column(feature)
conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE) authorized = user.project_authorizations.select(1).
authorized = user.authorized_projects.merge(conditional.reorder(nil)) where('project_authorizations.project_id = projects.id')
union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)]) with_project_feature.
where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql))) where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
visible,
ProjectFeature::PRIVATE,
authorized)
else
with_feature_access_level(feature, visible)
end
end end
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
......
...@@ -27,6 +27,13 @@ class ProjectFeature < ActiveRecord::Base ...@@ -27,6 +27,13 @@ class ProjectFeature < ActiveRecord::Base
"#{feature}_access_level".to_sym "#{feature}_access_level".to_sym
end end
def quoted_access_level_column(feature)
attribute = connection.quote_column_name(access_level_attribute(feature))
table = connection.quote_table_name(table_name)
"#{table}.#{attribute}"
end
end end
# Default scopes force us to unscope here since a service may need to check # Default scopes force us to unscope here since a service may need to check
......
...@@ -1003,8 +1003,6 @@ class User < ActiveRecord::Base ...@@ -1003,8 +1003,6 @@ class User < ActiveRecord::Base
def access_level def access_level
if admin? if admin?
:admin :admin
elsif auditor?
:auditor
else else
:regular :regular
end end
...@@ -1012,10 +1010,14 @@ class User < ActiveRecord::Base ...@@ -1012,10 +1010,14 @@ class User < ActiveRecord::Base
def access_level=(new_level) def access_level=(new_level)
new_level = new_level.to_s new_level = new_level.to_s
return unless %w(admin auditor regular).include?(new_level) return unless %w(admin regular).include?(new_level)
self.admin = (new_level == 'admin') self.admin = (new_level == 'admin')
self.auditor = (new_level == 'auditor') end
# Does the user have access to all private groups & projects?
def has_full_private_access?
admin?
end end
def update_two_factor_requirement def update_two_factor_requirement
......
---
title: Refactor ProjectsFinder#init_collection to produce more efficient queries for
retrieving projects
merge_request:
author:
...@@ -13,18 +13,8 @@ module Gitlab ...@@ -13,18 +13,8 @@ module Gitlab
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
scope :non_public_only, -> { where.not(visibility_level: PUBLIC) } scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
scope :public_to_user, -> (user) do scope :public_to_user, -> (user = nil) do
if user where(visibility_level: VisibilityLevel.levels_for_user(user))
if user.admin_or_auditor?
all
elsif !user.external?
public_and_internal_only
else
public_only
end
else
public_only
end
end end
end end
...@@ -35,6 +25,18 @@ module Gitlab ...@@ -35,6 +25,18 @@ module Gitlab
class << self class << self
delegate :values, to: :options delegate :values, to: :options
def levels_for_user(user = nil)
return [PUBLIC] unless user
if user.has_full_private_access?
[PRIVATE, INTERNAL, PUBLIC]
elsif user.external?
[PUBLIC]
else
[INTERNAL, PUBLIC]
end
end
def string_values def string_values
string_options.keys string_options.keys
end end
......
require 'spec_helper'
describe Gitlab::VisibilityLevel, lib: true do # rubocop:disable RSpec/FilePath
describe '.levels_for_user' do
it 'returns all levels for an auditor' do
user = build(:user, :auditor)
expect(described_class.levels_for_user(user)).
to eq([Gitlab::VisibilityLevel::PRIVATE,
Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC])
end
end
end
...@@ -18,4 +18,35 @@ describe Gitlab::VisibilityLevel, lib: true do ...@@ -18,4 +18,35 @@ describe Gitlab::VisibilityLevel, lib: true do
expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE) expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE)
end end
end end
describe '.levels_for_user' do
it 'returns all levels for an admin' do
user = build(:user, :admin)
expect(described_class.levels_for_user(user)).
to eq([Gitlab::VisibilityLevel::PRIVATE,
Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC])
end
it 'returns INTERNAL and PUBLIC for internal users' do
user = build(:user)
expect(described_class.levels_for_user(user)).
to eq([Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC])
end
it 'returns PUBLIC for external users' do
user = build(:user, :external)
expect(described_class.levels_for_user(user)).
to eq([Gitlab::VisibilityLevel::PUBLIC])
end
it 'returns PUBLIC when no user is given' do
expect(described_class.levels_for_user).
to eq([Gitlab::VisibilityLevel::PUBLIC])
end
end
end end
require 'spec_helper'
describe EE::User, models: true do
describe '#access_level=' do
let(:user) { build(:user) }
before do
# `auditor?` returns true only when the user is an auditor _and_ the auditor license
# add-on is present. We aren't testing this here, so we can assume that the add-on exists.
allow_any_instance_of(License).to receive(:feature_available?).with(:auditor_user) { true }
end
it "does not set 'auditor' for an invalid access level" do
user.access_level = :invalid_access_level
expect(user.auditor).to be false
end
it "does not set 'auditor' for admin level" do
user.access_level = :admin
expect(user.auditor).to be false
end
it "assigns the 'auditor' access level" do
user.access_level = :auditor
expect(user.access_level).to eq(:auditor)
expect(user.admin).to be false
expect(user.auditor).to be true
end
it "assigns the 'auditor' access level" do
user.access_level = :regular
expect(user.access_level).to eq(:regular)
expect(user.admin).to be false
expect(user.auditor).to be false
end
it "clears the 'admin' access level when a user is made an auditor" do
user.access_level = :admin
user.access_level = :auditor
expect(user.access_level).to eq(:auditor)
expect(user.admin).to be false
expect(user.auditor).to be true
end
it "clears the 'auditor' access level when a user is made an admin" do
user.access_level = :auditor
user.access_level = :admin
expect(user.access_level).to eq(:admin)
expect(user.admin).to be true
expect(user.auditor).to be false
end
it "doesn't clear existing 'auditor' access levels when an invalid access level is passed in" do
user.access_level = :auditor
user.access_level = :invalid_access_level
expect(user.access_level).to eq(:auditor)
expect(user.admin).to be false
expect(user.auditor).to be true
end
end
describe '#has_full_private_access?' do
it 'returns true for auditor user' do
user = build(:user, :auditor)
expect(user.has_full_private_access?).to be_truthy
end
end
end
...@@ -4,6 +4,18 @@ describe ProjectFeature do ...@@ -4,6 +4,18 @@ describe ProjectFeature do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
describe '.quoted_access_level_column' do
it 'returns the table name and quoted column name for a feature' do
expected = if Gitlab::Database.postgresql?
'"project_features"."issues_access_level"'
else
'`project_features`.`issues_access_level`'
end
expect(described_class.quoted_access_level_column(:issues)).to eq(expected)
end
end
describe '#feature_available?' do describe '#feature_available?' do
let(:features) { %w(issues wiki builds merge_requests snippets repository) } let(:features) { %w(issues wiki builds merge_requests snippets repository) }
......
...@@ -2552,4 +2552,36 @@ describe Project, models: true do ...@@ -2552,4 +2552,36 @@ describe Project, models: true do
expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i) expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i)
end end
end end
describe '.public_or_visible_to_user' do
let!(:user) { create(:user) }
let!(:private_project) do
create(:empty_project, :private, creator: user, namespace: user.namespace)
end
let!(:public_project) { create(:empty_project, :public) }
context 'with a user' do
let(:projects) do
Project.all.public_or_visible_to_user(user)
end
it 'includes projects the user has access to' do
expect(projects).to include(private_project)
end
it 'includes projects the user can see' do
expect(projects).to include(public_project)
end
end
context 'without a user' do
it 'only includes public projects' do
projects = Project.all.public_or_visible_to_user
expect(projects).to eq([public_project])
end
end
end
end end
...@@ -1737,18 +1737,11 @@ describe User, models: true do ...@@ -1737,18 +1737,11 @@ describe User, models: true do
describe '#access_level=' do describe '#access_level=' do
let(:user) { build(:user) } let(:user) { build(:user) }
before do
# `auditor?` returns true only when the user is an auditor _and_ the auditor license
# add-on is present. We aren't testing this here, so we can assume that the add-on exists.
allow_any_instance_of(License).to receive(:feature_available?).with(:auditor_user) { true }
end
it 'does nothing for an invalid access level' do it 'does nothing for an invalid access level' do
user.access_level = :invalid_access_level user.access_level = :invalid_access_level
expect(user.access_level).to eq(:regular) expect(user.access_level).to eq(:regular)
expect(user.admin).to be false expect(user.admin).to be false
expect(user.auditor).to be false
end end
it "assigns the 'admin' access level" do it "assigns the 'admin' access level" do
...@@ -1756,41 +1749,6 @@ describe User, models: true do ...@@ -1756,41 +1749,6 @@ describe User, models: true do
expect(user.access_level).to eq(:admin) expect(user.access_level).to eq(:admin)
expect(user.admin).to be true expect(user.admin).to be true
expect(user.auditor).to be false
end
it "assigns the 'auditor' access level" do
user.access_level = :auditor
expect(user.access_level).to eq(:auditor)
expect(user.admin).to be false
expect(user.auditor).to be true
end
it "assigns the 'auditor' access level" do
user.access_level = :regular
expect(user.access_level).to eq(:regular)
expect(user.admin).to be false
expect(user.auditor).to be false
end
it "clears the 'admin' access level when a user is made an auditor" do
user.access_level = :admin
user.access_level = :auditor
expect(user.access_level).to eq(:auditor)
expect(user.admin).to be false
expect(user.auditor).to be true
end
it "clears the 'auditor' access level when a user is made an admin" do
user.access_level = :auditor
user.access_level = :admin
expect(user.access_level).to eq(:admin)
expect(user.admin).to be true
expect(user.auditor).to be false
end end
it "doesn't clear existing access levels when an invalid access level is passed in" do it "doesn't clear existing access levels when an invalid access level is passed in" do
...@@ -1799,7 +1757,6 @@ describe User, models: true do ...@@ -1799,7 +1757,6 @@ describe User, models: true do
expect(user.access_level).to eq(:admin) expect(user.access_level).to eq(:admin)
expect(user.admin).to be true expect(user.admin).to be true
expect(user.auditor).to be false
end end
it "accepts string values in addition to symbols" do it "accepts string values in addition to symbols" do
...@@ -1807,7 +1764,20 @@ describe User, models: true do ...@@ -1807,7 +1764,20 @@ describe User, models: true do
expect(user.access_level).to eq(:admin) expect(user.access_level).to eq(:admin)
expect(user.admin).to be true expect(user.admin).to be true
expect(user.auditor).to be false end
end
describe '#has_full_private_access?' do
it 'returns false for regular user' do
user = build(:user)
expect(user.has_full_private_access?).to be_falsy
end
it 'returns true for admin user' do
user = build(:user, :admin)
expect(user.has_full_private_access?).to be_truthy
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