Skip to content
Snippets Groups Projects
Commit 148816cd authored by Bob Van Landuyt's avatar Bob Van Landuyt
Browse files

Port `read_cross_project` ability from EE

parent b5306075
No related branches found
No related tags found
No related merge requests found
Showing
with 337 additions and 47 deletions
class UsersController < ApplicationController
include RoutableActions
include RendersMemberAccess
include ControllerWithCrossProjectAccessCheck
requires_cross_project_access show: false,
groups: false,
projects: false,
contributed: false,
snippets: true,
calendar: false,
calendar_activities: true
 
skip_before_action :authenticate_user!
before_action :user, except: [:exists]
Loading
Loading
@@ -103,12 +112,7 @@ class UsersController < ApplicationController
end
 
def load_events
# Get user activity feed for projects common for both users
@events = user.recent_events
.merge(projects_for_current_user)
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
@events = UserRecentEventsFinder.new(current_user, user, params).execute
 
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
Loading
Loading
@@ -141,10 +145,6 @@ class UsersController < ApplicationController
).execute.page(params[:page])
end
 
def projects_for_current_user
ProjectsFinder.new(current_user: current_user).execute
end
def build_canonical_path(user)
url_for(params.merge(username: user.to_param))
end
Loading
Loading
module FinderMethods
def find_by!(*args)
raise_not_found_unless_authorized execute.find_by!(*args)
end
def find_by(*args)
if_authorized execute.find_by(*args)
end
def find(*args)
raise_not_found_unless_authorized model.find(*args)
end
private
def raise_not_found_unless_authorized(result)
result = if_authorized(result)
raise ActiveRecord::RecordNotFound.new("Couldn't find #{model}") unless result
result
end
def if_authorized(result)
# Return the result if the finder does not perform authorization checks.
# this is currently the case in the `MilestoneFinder`
return result unless respond_to?(:current_user)
if can_read_object?(result)
result
else
nil
end
end
def can_read_object?(object)
# When there's no policy, we'll allow the read, this is for example the case
# for Todos
return true unless DeclarativePolicy.has_policy?(object)
model_name = object&.model_name || model.model_name
Ability.allowed?(current_user, :"read_#{model_name.singular}", object)
end
# This fetches the model from the `ActiveRecord::Relation` but does not
# actually execute the query.
def model
execute.model
end
end
# Module to prepend into finders to specify wether or not the finder requires
# cross project access
#
# This module depends on the finder implementing the following methods:
#
# - `#execute` should return an `ActiveRecord::Relation`
# - `#current_user` the user that requires access (or nil)
module FinderWithCrossProjectAccess
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
extend Gitlab::CrossProjectAccess::ClassMethods
end
override :execute
def execute(*args)
check = Gitlab::CrossProjectAccess.find_check(self)
original = super
return original unless check
return original if should_skip_cross_project_check || can_read_cross_project?
if check.should_run?(self)
original.model.none
else
original
end
end
# We can skip the cross project check for finding indivitual records.
# this would be handled by the `can?(:read_*, result)` call in `FinderMethods`
# itself.
override :find_by!
def find_by!(*args)
skip_cross_project_check { super }
end
override :find_by
def find_by(*args)
skip_cross_project_check { super }
end
override :find
def find(*args)
skip_cross_project_check { super }
end
private
attr_accessor :should_skip_cross_project_check
def skip_cross_project_check
self.should_skip_cross_project_check = true
yield
ensure
# The find could raise an `ActiveRecord::RecordNotFound`, after which we
# still want to re-enable the check.
self.should_skip_cross_project_check = false
end
def can_read_cross_project?
Ability.allowed?(current_user, :read_cross_project)
end
def can_read_project?(project)
Ability.allowed?(current_user, :read_project, project)
end
end
class EventsFinder
prepend FinderMethods
prepend FinderWithCrossProjectAccess
attr_reader :source, :params, :current_user
 
requires_cross_project_access unless: -> { source.is_a?(Project) }
# Used to filter Events
#
# Arguments:
Loading
Loading
Loading
Loading
@@ -21,8 +21,12 @@
# my_reaction_emoji: string
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include CreatedAtFilter
 
requires_cross_project_access unless: -> { project? }
NONE = '0'.freeze
 
attr_accessor :current_user, :params
Loading
Loading
@@ -87,14 +91,6 @@ class IssuableFinder
by_my_reaction_emoji(items)
end
 
def find(*params)
execute.find(*params)
end
def find_by(*params)
execute.find_by(*params)
end
def row_count
Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
end
Loading
Loading
@@ -124,10 +120,6 @@ class IssuableFinder
counts
end
 
def find_by!(*params)
execute.find_by!(*params)
end
def group
return @group if defined?(@group)
 
Loading
Loading
class LabelsFinder < UnionFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include Gitlab::Utils::StrongMemoize
 
requires_cross_project_access unless: -> { project? }
def initialize(current_user, params = {})
@current_user = current_user
@params = params
Loading
Loading
class MergeRequestTargetProjectFinder
include FinderMethods
attr_reader :current_user, :source_project
 
def initialize(current_user: nil, source_project:)
Loading
Loading
Loading
Loading
@@ -8,6 +8,8 @@
# state - filters by state.
 
class MilestonesFinder
include FinderMethods
attr_reader :params, :project_ids, :group_ids
 
def initialize(params = {})
Loading
Loading
Loading
Loading
@@ -13,7 +13,9 @@
# params are optional
class SnippetsFinder < UnionFinder
include Gitlab::Allowable
attr_accessor :current_user, :params, :project
include FinderMethods
attr_accessor :current_user, :project, :params
 
def initialize(current_user, params = {})
@current_user = current_user
Loading
Loading
@@ -52,10 +54,14 @@ class SnippetsFinder < UnionFinder
end
 
def authorized_snippets
Snippet.where(feature_available_projects.or(not_project_related)).public_or_visible_to_user(current_user)
Snippet.where(feature_available_projects.or(not_project_related))
.public_or_visible_to_user(current_user)
end
 
def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part|
part.with_feature_available_for_user(:snippets, current_user)
end.select(:id)
Loading
Loading
Loading
Loading
@@ -13,6 +13,11 @@
#
 
class TodosFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
requires_cross_project_access unless: -> { project? }
NONE = '0'.freeze
 
attr_accessor :current_user, :params
Loading
Loading
# Get user activity feed for projects common for a user and a logged in user
#
# - current_user: The user viewing the events
# - user: The user for which to load the events
# - params:
# - offset: The page of events to return
class UserRecentEventsFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
requires_cross_project_access
attr_reader :current_user, :target_user, :params
def initialize(current_user, target_user, params = {})
@current_user = current_user
@target_user = target_user
@params = params
end
def execute
target_user
.recent_events
.merge(projects_for_current_user)
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
end
def projects_for_current_user
ProjectsFinder.new(current_user: current_user).execute
end
end
Loading
Loading
@@ -6,4 +6,28 @@ module DashboardHelper
def assigned_mrs_dashboard_path
merge_requests_dashboard_path(assignee_id: current_user.id)
end
def dashboard_nav_links
@dashboard_nav_links ||= get_dashboard_nav_links
end
def dashboard_nav_link?(link)
dashboard_nav_links.include?(link)
end
def any_dashboard_nav_link?(links)
links.any? { |link| dashboard_nav_link?(link) }
end
private
def get_dashboard_nav_links
links = [:projects, :groups, :snippets]
if can?(current_user, :read_cross_project)
links += [:activity, :milestones]
end
links
end
end
Loading
Loading
@@ -25,8 +25,24 @@ module ExploreHelper
controller.class.name.split("::").first == "Explore"
end
 
def explore_nav_links
@explore_nav_links ||= get_explore_nav_links
end
def explore_nav_link?(link)
explore_nav_links.include?(link)
end
def any_explore_nav_link?(links)
links.any? { |link| explore_nav_link?(link) }
end
private
 
def get_explore_nav_links
[:projects, :groups, :snippets]
end
def request_path_with_options(options = {})
request.path + "?#{options.to_param}"
end
Loading
Loading
Loading
Loading
@@ -3,6 +3,14 @@ module GroupsHelper
%w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
 
def group_sidebar_links
@group_sidebar_links ||= get_group_sidebar_links
end
def group_sidebar_link?(link)
group_sidebar_links.include?(link)
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
Loading
Loading
@@ -107,6 +115,20 @@ module GroupsHelper
 
private
 
def get_group_sidebar_links
links = [:overview, :group_members]
if can?(current_user, :read_cross_project)
links += [:activity, :issues, :labels, :milestones, :merge_requests]
end
if can?(current_user, :admin_group, @group)
links << :settings
end
links
end
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
output =
Loading
Loading
Loading
Loading
@@ -47,27 +47,6 @@ module IssuesHelper
end
end
 
def milestone_options(object)
milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed?
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
def project_options(issuable, current_user, ability: :read_project)
projects = current_user.authorized_projects.order_id_desc
projects = projects.select do |project|
current_user.can?(ability, project)
end
no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project')
projects.unshift(no_project)
projects.delete(issuable.project)
options_from_collection_for_select(projects, :id, :name_with_namespace)
end
def status_box_class(item)
if item.try(:expired?)
'status-box-expired'
Loading
Loading
module NavHelper
def header_links
@header_links ||= get_header_links
end
def header_link?(link)
header_links.include?(link)
end
def page_with_sidebar_class
class_name = page_gutter_class
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
Loading
Loading
@@ -38,4 +46,28 @@ module NavHelper
 
class_names
end
private
def get_header_links
links = if current_user
[:user_dropdown]
else
[:sign_in]
end
if can?(current_user, :read_cross_project)
links += [:issues, :merge_requests, :todos] if current_user.present?
end
if @project&.persisted? || can?(current_user, :read_cross_project)
links << :search
end
if session[:impersonator_id]
links << :admin_impersonation
end
links
end
end
Loading
Loading
@@ -213,6 +213,7 @@ module ProjectsHelper
controller.controller_name,
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
'v2.5'
]
 
Loading
Loading
@@ -608,4 +609,8 @@ module ProjectsHelper
 
project_find_file_path(@project, ref)
end
def can_show_last_commit_in_list?(project)
can?(current_user, :read_cross_project) && project.commit
end
end
Loading
Loading
@@ -14,4 +14,18 @@ module UsersHelper
content_tag(:strong) { user.unconfirmed_email } + h('.') +
content_tag(:p) { confirmation_link }
end
def profile_tabs
@profile_tabs ||= get_profile_tabs
end
def profile_tab?(tab)
profile_tabs.include?(tab)
end
private
def get_profile_tabs
[:activity, :groups, :contributed, :projects, :snippets]
end
end
Loading
Loading
@@ -22,12 +22,30 @@ class Ability
#
# 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)
# filters - A hash of abilities and filters to apply if the user lacks this
# ability
def issues_readable_by_user(issues, user = nil, filters: {})
issues = apply_filters_if_needed(issues, user, filters)
DeclarativePolicy.user_scope do
issues.select { |issue| issue.visible_to_user?(user) }
end
end
 
# Returns an Array of MergeRequests that can be read by the given user.
#
# merge_requests - MRs out of which to collect mr's readable by the user.
# user - The User for which to check the merge_requests
# filters - A hash of abilities and filters to apply if the user lacks this
# ability
def merge_requests_readable_by_user(merge_requests, user = nil, filters: {})
merge_requests = apply_filters_if_needed(merge_requests, user, filters)
DeclarativePolicy.user_scope do
merge_requests.select { |mr| allowed?(user, :read_merge_request, mr) }
end
end
def can_edit_note?(user, note)
allowed?(user, :edit_note, note)
end
Loading
Loading
@@ -53,5 +71,15 @@ class Ability
cache = RequestStore.active? ? RequestStore : {}
DeclarativePolicy.policy_for(user, subject, cache: cache)
end
private
def apply_filters_if_needed(elements, user, filters)
filters.each do |ability, filter|
elements = filter.call(elements) unless allowed?(user, ability)
end
elements
end
end
end
Loading
Loading
@@ -35,6 +35,7 @@ module ProtectedRefAccess
def check_access(user)
return true if user.admin?
 
project.team.max_member_access(user.id) >= access_level
user.can?(:push_code, project) &&
project.team.max_member_access(user.id) >= access_level
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment