Commit ddca3ddd authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'security-guests-can-see-list-of-merge-requests' into 'master'

[master] Group Guests are no longer able to see merge requests

See merge request gitlab/gitlabhq!2694
parents 40d99004 9ec860ea
...@@ -377,8 +377,10 @@ class Project < ActiveRecord::Base ...@@ -377,8 +377,10 @@ class Project < ActiveRecord::Base
# "enabled" here means "not disabled". It includes private features! # "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) { scope :with_feature_enabled, ->(feature) {
access_level_attribute = ProjectFeature.access_level_attribute(feature) access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] }) enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))
with_project_feature.where(enabled_feature)
} }
# Picks a feature where the level is exactly that given. # Picks a feature where the level is exactly that given.
...@@ -465,7 +467,8 @@ class Project < ActiveRecord::Base ...@@ -465,7 +467,8 @@ class Project < ActiveRecord::Base
# logged in users to more efficiently get private projects with the given # logged in users to more efficiently get private projects with the given
# feature. # feature.
def self.with_feature_available_for_user(feature, user) def self.with_feature_available_for_user(feature, user)
visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
min_access_level = ProjectFeature.required_minimum_access_level(feature)
if user&.admin? if user&.admin?
with_feature_enabled(feature) with_feature_enabled(feature)
...@@ -473,10 +476,15 @@ class Project < ActiveRecord::Base ...@@ -473,10 +476,15 @@ class Project < ActiveRecord::Base
column = ProjectFeature.quoted_access_level_column(feature) column = ProjectFeature.quoted_access_level_column(feature)
with_project_feature with_project_feature
.where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", .where(
visible, "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
ProjectFeature::PRIVATE, " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
user.authorizations_for_projects) {
private: Gitlab::VisibilityLevel::PRIVATE,
public_visible: ProjectFeature::ENABLED,
private_visible: ProjectFeature::PRIVATE,
authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
})
else else
with_feature_access_level(feature, visible) with_feature_access_level(feature, visible)
end end
......
...@@ -23,11 +23,11 @@ class ProjectFeature < ActiveRecord::Base ...@@ -23,11 +23,11 @@ class ProjectFeature < ActiveRecord::Base
PUBLIC = 30 PUBLIC = 30
FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
class << self class << self
def access_level_attribute(feature) def access_level_attribute(feature)
feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name) feature = ensure_feature!(feature)
raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
"#{feature}_access_level".to_sym "#{feature}_access_level".to_sym
end end
...@@ -38,6 +38,21 @@ class ProjectFeature < ActiveRecord::Base ...@@ -38,6 +38,21 @@ class ProjectFeature < ActiveRecord::Base
"#{table}.#{attribute}" "#{table}.#{attribute}"
end end
def required_minimum_access_level(feature)
feature = ensure_feature!(feature)
PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST)
end
private
def ensure_feature!(feature)
feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
feature
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
......
...@@ -754,8 +754,12 @@ class User < ActiveRecord::Base ...@@ -754,8 +754,12 @@ class User < ActiveRecord::Base
# #
# Example use: # Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)` # `Project.where('EXISTS(?)', user.authorizations_for_projects)`
def authorizations_for_projects def authorizations_for_projects(min_access_level: nil)
project_authorizations.select(1).where('project_authorizations.project_id = projects.id') authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
return authorizations unless min_access_level.present?
authorizations.where('project_authorizations.access_level >= ?', min_access_level)
end end
# Returns the projects this user has reporter (or greater) access to, limited # Returns the projects this user has reporter (or greater) access to, limited
......
---
title: Group guests are no longer able to see merge requests they don't have access
to at group level
merge_request:
author:
type: security
...@@ -68,7 +68,8 @@ describe MergeRequestsFinder do ...@@ -68,7 +68,8 @@ describe MergeRequestsFinder do
expect(merge_requests.size).to eq(2) expect(merge_requests.size).to eq(2)
end end
it 'filters by group' do context 'filtering by group' do
it 'includes all merge requests when user has access' do
params = { group_id: group.id } params = { group_id: group.id }
merge_requests = described_class.new(user, params).execute merge_requests = described_class.new(user, params).execute
...@@ -76,6 +77,18 @@ describe MergeRequestsFinder do ...@@ -76,6 +77,18 @@ describe MergeRequestsFinder do
expect(merge_requests.size).to eq(3) expect(merge_requests.size).to eq(3)
end 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 it 'filters by group including subgroups', :nested_groups do
params = { group_id: group.id, include_subgroups: true } params = { group_id: group.id, include_subgroups: true }
...@@ -83,6 +96,7 @@ describe MergeRequestsFinder do ...@@ -83,6 +96,7 @@ describe MergeRequestsFinder do
expect(merge_requests.size).to eq(6) expect(merge_requests.size).to eq(6)
end end
end
it 'filters by non_archived' do it 'filters by non_archived' do
params = { non_archived: true } params = { non_archived: true }
......
...@@ -3074,6 +3074,66 @@ describe Project do ...@@ -3074,6 +3074,66 @@ describe Project do
end end
end end
describe '.with_feature_available_for_user' do
let!(:user) { create(:user) }
let!(:feature) { MergeRequest }
let!(:project) { create(:project, :public, :merge_requests_enabled) }
subject { described_class.with_feature_available_for_user(feature, user) }
context 'when user has access to project' do
subject { described_class.with_feature_available_for_user(feature, user) }
before do
project.add_guest(user)
end
context 'when public project' do
context 'when feature is public' do
it 'returns project' do
is_expected.to include(project)
end
end
context 'when feature is private' do
let!(:project) { create(:project, :public, :merge_requests_private) }
it 'returns project when user has access to the feature' do
project.add_maintainer(user)
is_expected.to include(project)
end
it 'does not return project when user does not have the minimum access level required' do
is_expected.not_to include(project)
end
end
end
context 'when private project' do
let!(:project) { create(:project) }
it 'returns project when user has access to the feature' do
project.add_maintainer(user)
is_expected.to include(project)
end
it 'does not return project when user does not have the minimum access level required' do
is_expected.not_to include(project)
end
end
end
context 'when user does not have access to project' do
let!(:project) { create(:project) }
it 'does not return project when user cant access project' do
is_expected.not_to include(project)
end
end
end
describe '#pages_available?' do describe '#pages_available?' do
let(:project) { create(:project, group: group) } let(:project) { create(:project, group: group) }
......
...@@ -1997,6 +1997,33 @@ describe User do ...@@ -1997,6 +1997,33 @@ describe User do
expect(subject).to include(accessible) expect(subject).to include(accessible)
expect(subject).not_to include(other) expect(subject).not_to include(other)
end end
context 'with min_access_level' do
let!(:user) { create(:user) }
let!(:project) { create(:project, :private, namespace: user.namespace) }
before do
project.add_developer(user)
end
subject { Project.where("EXISTS (?)", user.authorizations_for_projects(min_access_level: min_access_level)) }
context 'when developer access' do
let(:min_access_level) { Gitlab::Access::DEVELOPER }
it 'includes projects a user has access to' do
expect(subject).to include(project)
end
end
context 'when owner access' do
let(:min_access_level) { Gitlab::Access::OWNER }
it 'does not include projects with higher access level' do
expect(subject).not_to include(project)
end
end
end
end end
describe '#authorized_projects', :delete do describe '#authorized_projects', :delete do
......
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