Merge branch 'bvl-external-auth-port' into 'master'
Port `read_cross_project` ability from EE See merge request gitlab-org/gitlab-ce!17208
This commit is contained in:
commit
f4bc6ec92e
114 changed files with 2509 additions and 348 deletions
|
@ -126,10 +126,15 @@ class ApplicationController < ActionController::Base
|
|||
Ability.allowed?(object, action, subject)
|
||||
end
|
||||
|
||||
def access_denied!
|
||||
def access_denied!(message = nil)
|
||||
respond_to do |format|
|
||||
format.json { head :not_found }
|
||||
format.any { render "errors/access_denied", layout: "errors", status: 404 }
|
||||
format.any { head :not_found }
|
||||
format.html do
|
||||
render "errors/access_denied",
|
||||
layout: "errors",
|
||||
status: 404,
|
||||
locals: { message: message }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ module Boards
|
|||
end
|
||||
|
||||
def issue
|
||||
@issue ||= issues_finder.execute.find(params[:id])
|
||||
@issue ||= issues_finder.find(params[:id])
|
||||
end
|
||||
|
||||
def filter_params
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
module ControllerWithCrossProjectAccessCheck
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
extend Gitlab::CrossProjectAccess::ClassMethods
|
||||
before_action :cross_project_check
|
||||
end
|
||||
|
||||
def cross_project_check
|
||||
if Gitlab::CrossProjectAccess.find_check(self)&.should_run?(self)
|
||||
authorize_cross_project_page!
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_cross_project_page!
|
||||
return if can?(current_user, :read_cross_project)
|
||||
|
||||
rejection_message = _(
|
||||
"This page is unavailable because you are not allowed to read information "\
|
||||
"across multiple projects."
|
||||
)
|
||||
access_denied!(rejection_message)
|
||||
end
|
||||
end
|
|
@ -3,16 +3,20 @@ module RoutableActions
|
|||
|
||||
def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
|
||||
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
|
||||
|
||||
if routable_authorized?(routable, extra_authorization_proc)
|
||||
ensure_canonical_path(routable, requested_full_path)
|
||||
routable
|
||||
else
|
||||
route_not_found
|
||||
handle_not_found_or_authorized(routable)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# This is overridden in gitlab-ee.
|
||||
def handle_not_found_or_authorized(_routable)
|
||||
route_not_found
|
||||
end
|
||||
|
||||
def routable_authorized?(routable, extra_authorization_proc)
|
||||
action = :"read_#{routable.class.to_s.underscore}"
|
||||
return false unless can?(current_user, action, routable)
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
class Dashboard::ApplicationController < ApplicationController
|
||||
include ControllerWithCrossProjectAccessCheck
|
||||
|
||||
layout 'dashboard'
|
||||
|
||||
requires_cross_project_access
|
||||
|
||||
private
|
||||
|
||||
def projects
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
class Dashboard::GroupsController < Dashboard::ApplicationController
|
||||
include GroupTree
|
||||
|
||||
skip_cross_project_access_check :index
|
||||
|
||||
def index
|
||||
groups = GroupsFinder.new(current_user, all_available: false).execute
|
||||
render_group_tree(groups)
|
||||
|
|
|
@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
|
|||
|
||||
before_action :set_non_archived_param
|
||||
before_action :default_sorting
|
||||
skip_cross_project_access_check :index, :starred
|
||||
|
||||
def index
|
||||
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Dashboard::SnippetsController < Dashboard::ApplicationController
|
||||
skip_cross_project_access_check :index
|
||||
|
||||
def index
|
||||
@snippets = SnippetsFinder.new(
|
||||
current_user,
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
class Groups::ApplicationController < ApplicationController
|
||||
include RoutableActions
|
||||
include ControllerWithCrossProjectAccessCheck
|
||||
|
||||
layout 'group'
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
before_action :group
|
||||
requires_cross_project_access
|
||||
|
||||
private
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
class Groups::AvatarsController < Groups::ApplicationController
|
||||
before_action :authorize_admin_group!
|
||||
|
||||
skip_cross_project_access_check :destroy
|
||||
|
||||
def destroy
|
||||
@group.remove_avatar!
|
||||
@group.save
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
module Groups
|
||||
class ChildrenController < Groups::ApplicationController
|
||||
before_action :group
|
||||
skip_cross_project_access_check :index
|
||||
|
||||
def index
|
||||
parent = if params[:parent_id].present?
|
||||
|
|
|
@ -6,6 +6,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
# Authorize
|
||||
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
|
||||
|
||||
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
|
||||
:approve_access_request, :leave, :resend_invite,
|
||||
:override
|
||||
|
||||
def index
|
||||
@sort = params[:sort].presence || sort_value_name
|
||||
@project = @group.projects.find(params[:project_id]) if params[:project_id]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
module Groups
|
||||
module Settings
|
||||
class CiCdController < Groups::ApplicationController
|
||||
skip_cross_project_access_check :show
|
||||
before_action :authorize_admin_pipeline!
|
||||
|
||||
def show
|
||||
|
|
|
@ -2,6 +2,8 @@ module Groups
|
|||
class VariablesController < Groups::ApplicationController
|
||||
before_action :authorize_admin_build!
|
||||
|
||||
skip_cross_project_access_check :show, :update
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
|
|
|
@ -19,6 +19,12 @@ class GroupsController < Groups::ApplicationController
|
|||
|
||||
before_action :user_actions, only: [:show, :subgroups]
|
||||
|
||||
skip_cross_project_access_check :index, :new, :create, :edit, :update,
|
||||
:destroy, :projects
|
||||
# When loading show as an atom feed, we render events that could leak cross
|
||||
# project information
|
||||
skip_cross_project_access_check :show, if: -> { request.format.html? }
|
||||
|
||||
layout :determine_layout
|
||||
|
||||
def index
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
||||
include Gitlab::GonHelper
|
||||
include Gitlab::Allowable
|
||||
include PageLayoutHelper
|
||||
include OauthApplications
|
||||
|
||||
|
@ -8,6 +9,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
|||
before_action :add_gon_variables
|
||||
before_action :load_scopes, only: [:index, :create, :edit]
|
||||
|
||||
helper_method :can?
|
||||
|
||||
layout 'profile'
|
||||
|
||||
def index
|
||||
|
|
|
@ -34,9 +34,9 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
|
|||
def target
|
||||
case params[:type]&.downcase
|
||||
when 'issue'
|
||||
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
|
||||
IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id])
|
||||
when 'mergerequest'
|
||||
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
|
||||
MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id])
|
||||
when 'commit'
|
||||
@project.commit(params[:type_id])
|
||||
end
|
||||
|
|
|
@ -133,7 +133,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def after_edit_path
|
||||
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
|
||||
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid])
|
||||
if from_merge_request && @branch_name == @ref
|
||||
diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) +
|
||||
"##{hexdigest(@path)}"
|
||||
|
|
|
@ -75,7 +75,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
|
|||
def branch_to
|
||||
@target_project = selected_target_project
|
||||
|
||||
if params[:ref].present?
|
||||
if @target_project && params[:ref].present?
|
||||
@ref = params[:ref]
|
||||
@commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
|
||||
end
|
||||
|
@ -85,7 +85,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
|
|||
|
||||
def update_branches
|
||||
@target_project = selected_target_project
|
||||
@target_branches = @target_project.repository.branch_names
|
||||
@target_branches = @target_project ? @target_project.repository.branch_names : []
|
||||
|
||||
render layout: false
|
||||
end
|
||||
|
@ -121,7 +121,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
|
|||
@project
|
||||
elsif params[:target_project_id].present?
|
||||
MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project)
|
||||
.execute.find(params[:target_project_id])
|
||||
.find_by(id: params[:target_project_id])
|
||||
else
|
||||
@project.forked_from_project
|
||||
end
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
class SearchController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
include ControllerWithCrossProjectAccessCheck
|
||||
include SearchHelper
|
||||
include RendersCommits
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
requires_cross_project_access if: -> do
|
||||
search_term_present = params[:search].present? || params[:term].present?
|
||||
search_term_present && !params[:project_id].present?
|
||||
end
|
||||
|
||||
layout 'search'
|
||||
|
||||
def show
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
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]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
|
51
app/finders/concerns/finder_methods.rb
Normal file
51
app/finders/concerns/finder_methods.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
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
|
70
app/finders/concerns/finder_with_cross_project_access.rb
Normal file
70
app/finders/concerns/finder_with_cross_project_access.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
# 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
|
|
@ -1,6 +1,10 @@
|
|||
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:
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -124,10 +120,6 @@ class IssuableFinder
|
|||
counts
|
||||
end
|
||||
|
||||
def find_by!(*params)
|
||||
execute.find_by!(*params)
|
||||
end
|
||||
|
||||
def group
|
||||
return @group if defined?(@group)
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
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
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class MergeRequestTargetProjectFinder
|
||||
include FinderMethods
|
||||
|
||||
attr_reader :current_user, :source_project
|
||||
|
||||
def initialize(current_user: nil, source_project:)
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
# state - filters by state.
|
||||
|
||||
class MilestonesFinder
|
||||
include FinderMethods
|
||||
|
||||
attr_reader :params, :project_ids, :group_ids
|
||||
|
||||
def initialize(params = {})
|
||||
|
|
|
@ -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
|
||||
|
@ -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)
|
||||
|
|
|
@ -13,6 +13,11 @@
|
|||
#
|
||||
|
||||
class TodosFinder
|
||||
prepend FinderWithCrossProjectAccess
|
||||
include FinderMethods
|
||||
|
||||
requires_cross_project_access unless: -> { project? }
|
||||
|
||||
NONE = '0'.freeze
|
||||
|
||||
attr_accessor :current_user, :params
|
||||
|
|
33
app/finders/user_recent_events_finder.rb
Normal file
33
app/finders/user_recent_events_finder.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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 =
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
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
|
||||
|
@ -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
|
||||
|
|
|
@ -208,6 +208,7 @@ module ProjectsHelper
|
|||
controller.controller_name,
|
||||
controller.action_name,
|
||||
Gitlab::CurrentSettings.cache_key,
|
||||
"cross-project:#{can?(current_user, :read_cross_project)}",
|
||||
'v2.5'
|
||||
]
|
||||
|
||||
|
@ -526,4 +527,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -159,7 +159,18 @@ class Issue < ActiveRecord::Base
|
|||
object.all_references(current_user, extractor: ext)
|
||||
end
|
||||
|
||||
ext.merge_requests.sort_by(&:iid)
|
||||
merge_requests = ext.merge_requests.sort_by(&:iid)
|
||||
|
||||
cross_project_filter = -> (merge_requests) do
|
||||
merge_requests.select { |mr| mr.target_project == project }
|
||||
end
|
||||
|
||||
Ability.merge_requests_readable_by_user(
|
||||
merge_requests, current_user,
|
||||
filters: {
|
||||
read_cross_project: cross_project_filter
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# All branches containing the current issue's ID, except for
|
||||
|
|
|
@ -85,6 +85,7 @@ class NotificationRecipient
|
|||
return false unless user.can?(:receive_notifications)
|
||||
return true if @skip_read_ability
|
||||
|
||||
return false if @target && !user.can?(:read_cross_project)
|
||||
return false if @project && !user.can?(:read_project, @project)
|
||||
|
||||
return true unless read_ability
|
||||
|
|
|
@ -1037,6 +1037,9 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def user_can_push_to_empty_repo?(user)
|
||||
return false unless empty_repo?
|
||||
return false unless Ability.allowed?(user, :push_code, self)
|
||||
|
||||
!ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
|
||||
end
|
||||
|
||||
|
|
|
@ -15,4 +15,7 @@ class BasePolicy < DeclarativePolicy::Base
|
|||
condition(:restricted_public_level, scope: :global) do
|
||||
Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
|
||||
end
|
||||
|
||||
# This is prevented in some cases in `gitlab-ee`
|
||||
rule { default }.enable :read_cross_project
|
||||
end
|
||||
|
|
|
@ -3,6 +3,19 @@ class IssuablePolicy < BasePolicy
|
|||
|
||||
condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? }
|
||||
|
||||
# We aren't checking `:read_issue` or `:read_merge_request` in this case
|
||||
# because it could be possible for a user to see an issuable-iid
|
||||
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be allowed
|
||||
# to read the actual issue after a more expensive `:read_issue` check.
|
||||
#
|
||||
# `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee.
|
||||
condition(:visible_to_user, score: 4) do
|
||||
Project.where(id: @subject.project)
|
||||
.public_or_visible_to_user(@user)
|
||||
.with_feature_available_for_user(@subject, @user)
|
||||
.any?
|
||||
end
|
||||
|
||||
condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) }
|
||||
|
||||
desc "User is the assignee or author"
|
||||
|
|
|
@ -13,7 +13,10 @@ class IssuePolicy < IssuablePolicy
|
|||
|
||||
rule { confidential & ~can_read_confidential }.policy do
|
||||
prevent :read_issue
|
||||
prevent :read_issue_iid
|
||||
prevent :update_issue
|
||||
prevent :admin_issue
|
||||
end
|
||||
|
||||
rule { can?(:read_issue) | visible_to_user }.enable :read_issue_iid
|
||||
end
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
class MergeRequestPolicy < IssuablePolicy
|
||||
# pass
|
||||
rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid
|
||||
end
|
||||
|
|
|
@ -80,8 +80,9 @@ class ProjectPolicy < BasePolicy
|
|||
rule { reporter }.enable :reporter_access
|
||||
rule { developer }.enable :developer_access
|
||||
rule { master }.enable :master_access
|
||||
rule { owner | admin }.enable :owner_access
|
||||
|
||||
rule { owner | admin }.policy do
|
||||
rule { can?(:owner_access) }.policy do
|
||||
enable :guest_access
|
||||
enable :reporter_access
|
||||
enable :developer_access
|
||||
|
@ -98,11 +99,6 @@ class ProjectPolicy < BasePolicy
|
|||
enable :remove_pages
|
||||
end
|
||||
|
||||
rule { owner | reporter }.policy do
|
||||
enable :build_download_code
|
||||
enable :build_read_container_image
|
||||
end
|
||||
|
||||
rule { can?(:guest_access) }.policy do
|
||||
enable :read_project
|
||||
enable :read_board
|
||||
|
@ -121,6 +117,11 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_cycle_analytics
|
||||
end
|
||||
|
||||
# These abilities are not allowed to admins that are not members of the project,
|
||||
# that's why they are defined separatly.
|
||||
rule { guest & can?(:download_code) }.enable :build_download_code
|
||||
rule { guest & can?(:read_container_image) }.enable :build_read_container_image
|
||||
|
||||
rule { can?(:reporter_access) }.policy do
|
||||
enable :download_code
|
||||
enable :download_wiki_code
|
||||
|
@ -140,12 +141,19 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_merge_request
|
||||
end
|
||||
|
||||
# We define `:public_user_access` separately because there are cases in gitlab-ee
|
||||
# where we enable or prevent it based on other coditions.
|
||||
rule { (~anonymous & public_project) | internal_access }.policy do
|
||||
enable :public_user_access
|
||||
end
|
||||
|
||||
rule { can?(:public_user_access) }.policy do
|
||||
enable :public_access
|
||||
enable :guest_access
|
||||
|
||||
enable :fork_project
|
||||
enable :build_download_code
|
||||
enable :build_read_container_image
|
||||
enable :request_access
|
||||
end
|
||||
|
||||
|
@ -196,14 +204,6 @@ class ProjectPolicy < BasePolicy
|
|||
enable :create_cluster
|
||||
end
|
||||
|
||||
rule { can?(:public_user_access) }.policy do
|
||||
enable :public_access
|
||||
|
||||
enable :fork_project
|
||||
enable :build_download_code
|
||||
enable :build_read_container_image
|
||||
end
|
||||
|
||||
rule { archived }.policy do
|
||||
prevent :create_merge_request
|
||||
prevent :push_code
|
||||
|
|
|
@ -11,9 +11,7 @@ class GroupChildEntity < Grape::Entity
|
|||
end
|
||||
|
||||
expose :can_edit do |instance|
|
||||
return false unless request.respond_to?(:current_user)
|
||||
|
||||
can?(request.current_user, "admin_#{type}", instance)
|
||||
can_edit?
|
||||
end
|
||||
|
||||
expose :edit_path do |instance|
|
||||
|
@ -83,4 +81,17 @@ class GroupChildEntity < Grape::Entity
|
|||
def markdown_description
|
||||
markdown_field(object, :description)
|
||||
end
|
||||
|
||||
def can_edit?
|
||||
return false unless request.respond_to?(:current_user)
|
||||
|
||||
if project?
|
||||
# Avoid checking rights for each project, as it might be expensive if the
|
||||
# user cannot read cross project.
|
||||
can?(request.current_user, :read_cross_project) &&
|
||||
can?(request.current_user, :admin_project, object)
|
||||
else
|
||||
can?(request.current_user, :admin_group, object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -247,7 +247,7 @@ class IssuableBaseService < BaseService
|
|||
when 'add'
|
||||
todo_service.mark_todo(issuable, current_user)
|
||||
when 'done'
|
||||
todo = TodosFinder.new(current_user).execute.find_by(target: issuable)
|
||||
todo = TodosFinder.new(current_user).find_by(target: issuable)
|
||||
todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- message = local_assigns.fetch(:message)
|
||||
|
||||
- content_for(:title, 'Access Denied')
|
||||
%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
|
||||
%h1
|
||||
|
@ -5,5 +7,9 @@
|
|||
.container
|
||||
%h3 Access Denied
|
||||
%hr
|
||||
%p You are not allowed to access this page.
|
||||
%p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"}
|
||||
- if message
|
||||
%p
|
||||
= message
|
||||
- else
|
||||
%p You are not allowed to access this page.
|
||||
%p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"}
|
||||
|
|
|
@ -20,29 +20,34 @@
|
|||
%ul.nav.navbar-nav
|
||||
- if current_user
|
||||
= render 'layouts/header/new_dropdown'
|
||||
%li.hidden-sm.hidden-xs
|
||||
= render 'layouts/search' unless current_controller?(:search)
|
||||
%li.visible-sm-inline-block.visible-xs-inline-block
|
||||
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
|
||||
= sprite_icon('search', size: 16)
|
||||
- if current_user
|
||||
- if header_link?(:search)
|
||||
%li.hidden-sm.hidden-xs
|
||||
= render 'layouts/search' unless current_controller?(:search)
|
||||
%li.visible-sm-inline-block.visible-xs-inline-block
|
||||
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
|
||||
= sprite_icon('search', size: 16)
|
||||
|
||||
- if header_link?(:issues)
|
||||
= nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
|
||||
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
|
||||
= sprite_icon('issues', size: 16)
|
||||
- issues_count = assigned_issuables_count(:issues)
|
||||
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
|
||||
= number_with_delimiter(issues_count)
|
||||
- if header_link?(:merge_requests)
|
||||
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
|
||||
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
|
||||
= sprite_icon('git-merge', size: 16)
|
||||
- merge_requests_count = assigned_issuables_count(:merge_requests)
|
||||
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
|
||||
= number_with_delimiter(merge_requests_count)
|
||||
- if header_link?(:todos)
|
||||
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
|
||||
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
|
||||
= sprite_icon('todo-done', size: 16)
|
||||
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
|
||||
= todos_count_format(todos_pending_count)
|
||||
- if header_link?(:user_dropdown)
|
||||
%li.header-user.dropdown
|
||||
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
|
||||
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
|
||||
|
@ -64,11 +69,11 @@
|
|||
%li.divider
|
||||
%li
|
||||
= link_to "Sign out", destroy_user_session_path, class: "sign-out-link"
|
||||
- if session[:impersonator_id]
|
||||
%li.impersonation
|
||||
= link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||
= icon('user-secret')
|
||||
- else
|
||||
- if header_link?(:admin_impersonation)
|
||||
%li.impersonation
|
||||
= link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||
= icon('user-secret')
|
||||
- if header_link?(:sign_in)
|
||||
%li
|
||||
%div
|
||||
= link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
|
||||
|
|
|
@ -1,53 +1,64 @@
|
|||
%ul.list-unstyled.navbar-sub-nav
|
||||
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do
|
||||
%a{ href: "#", data: { toggle: "dropdown" } }
|
||||
Projects
|
||||
= sprite_icon('angle-down', css_class: 'caret-down')
|
||||
.dropdown-menu.projects-dropdown-menu
|
||||
= render "layouts/nav/projects_dropdown/show"
|
||||
- if dashboard_nav_link?(:projects)
|
||||
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do
|
||||
%a{ href: "#", data: { toggle: "dropdown" } }
|
||||
Projects
|
||||
= sprite_icon('angle-down', css_class: 'caret-down')
|
||||
.dropdown-menu.projects-dropdown-menu
|
||||
= render "layouts/nav/projects_dropdown/show"
|
||||
|
||||
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
|
||||
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do
|
||||
Groups
|
||||
- if dashboard_nav_link?(:groups)
|
||||
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
|
||||
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do
|
||||
Groups
|
||||
|
||||
= nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
|
||||
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
|
||||
Activity
|
||||
- if dashboard_nav_link?(:activity)
|
||||
= nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
|
||||
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
|
||||
Activity
|
||||
|
||||
= nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
|
||||
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
|
||||
Milestones
|
||||
- if dashboard_nav_link?(:milestones)
|
||||
= nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
|
||||
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
|
||||
Milestones
|
||||
|
||||
= nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
|
||||
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
|
||||
Snippets
|
||||
- if dashboard_nav_link?(:snippets)
|
||||
= nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
|
||||
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
|
||||
Snippets
|
||||
|
||||
%li.header-more.dropdown.hidden-lg
|
||||
%a{ href: "#", data: { toggle: "dropdown" } }
|
||||
More
|
||||
= sprite_icon('angle-down', css_class: 'caret-down')
|
||||
.dropdown-menu
|
||||
%ul
|
||||
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
|
||||
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
|
||||
Groups
|
||||
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
|
||||
%li.header-more.dropdown.hidden-lg
|
||||
%a{ href: "#", data: { toggle: "dropdown" } }
|
||||
More
|
||||
= sprite_icon('angle-down', css_class: 'caret-down')
|
||||
.dropdown-menu
|
||||
%ul
|
||||
- if dashboard_nav_link?(:groups)
|
||||
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
|
||||
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
|
||||
Groups
|
||||
|
||||
= nav_link(path: 'dashboard#activity') do
|
||||
= link_to activity_dashboard_path, title: 'Activity' do
|
||||
Activity
|
||||
- if dashboard_nav_link?(:activity)
|
||||
= nav_link(path: 'dashboard#activity') do
|
||||
= link_to activity_dashboard_path, title: 'Activity' do
|
||||
Activity
|
||||
|
||||
= nav_link(controller: 'dashboard/milestones') do
|
||||
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
|
||||
Milestones
|
||||
- if dashboard_nav_link?(:milestones)
|
||||
= nav_link(controller: 'dashboard/milestones') do
|
||||
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
|
||||
Milestones
|
||||
|
||||
= nav_link(controller: 'dashboard/snippets') do
|
||||
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
|
||||
Snippets
|
||||
- if dashboard_nav_link?(:snippets)
|
||||
= nav_link(controller: 'dashboard/snippets') do
|
||||
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
|
||||
Snippets
|
||||
|
||||
-# Shortcut to Dashboard > Projects
|
||||
%li.hidden
|
||||
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
|
||||
Projects
|
||||
- if dashboard_nav_link?(:projects)
|
||||
%li.hidden
|
||||
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
|
||||
Projects
|
||||
|
||||
- if current_controller?('ide')
|
||||
%li.line-separator.hidden-xs
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
%ul.list-unstyled.navbar-sub-nav
|
||||
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
|
||||
= link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
|
||||
Projects
|
||||
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
|
||||
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
|
||||
Groups
|
||||
= nav_link(controller: :snippets) do
|
||||
= link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
|
||||
Snippets
|
||||
- if explore_nav_link?(:projects)
|
||||
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
|
||||
= link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
|
||||
Projects
|
||||
- if explore_nav_link?(:groups)
|
||||
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
|
||||
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
|
||||
Groups
|
||||
- if explore_nav_link?(:snippets)
|
||||
= nav_link(controller: :snippets) do
|
||||
= link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
|
||||
Snippets
|
||||
%li
|
||||
= link_to "Help", help_path, title: 'About GitLab CE'
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
- issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count
|
||||
- merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count
|
||||
|
||||
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
|
||||
|
||||
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
|
||||
.nav-sidebar-inner-scroll
|
||||
.context-header
|
||||
|
@ -10,84 +12,93 @@
|
|||
.sidebar-context-title
|
||||
= @group.name
|
||||
%ul.sidebar-top-level-items
|
||||
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
|
||||
= link_to group_path(@group) do
|
||||
.nav-icon-container
|
||||
= sprite_icon('project')
|
||||
%span.nav-item-name
|
||||
Overview
|
||||
- if group_sidebar_link?(:overview)
|
||||
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do
|
||||
= link_to group_path(@group) do
|
||||
.nav-icon-container
|
||||
= sprite_icon('project')
|
||||
%span.nav-item-name
|
||||
Overview
|
||||
|
||||
%ul.sidebar-sub-level-items
|
||||
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to group_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Overview') }
|
||||
%li.divider.fly-out-top-item
|
||||
= nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
|
||||
= link_to group_path(@group), title: 'Group details' do
|
||||
%span
|
||||
Details
|
||||
%ul.sidebar-sub-level-items
|
||||
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to group_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Overview') }
|
||||
%li.divider.fly-out-top-item
|
||||
= nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
|
||||
= link_to group_path(@group), title: 'Group details' do
|
||||
%span
|
||||
Details
|
||||
|
||||
= nav_link(path: 'groups#activity') do
|
||||
= link_to activity_group_path(@group), title: 'Activity' do
|
||||
%span
|
||||
Activity
|
||||
- if group_sidebar_link?(:activity)
|
||||
= nav_link(path: 'groups#activity') do
|
||||
= link_to activity_group_path(@group), title: 'Activity' do
|
||||
%span
|
||||
Activity
|
||||
|
||||
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
|
||||
= link_to issues_group_path(@group) do
|
||||
.nav-icon-container
|
||||
= sprite_icon('issues')
|
||||
%span.nav-item-name
|
||||
Issues
|
||||
%span.badge.count= number_with_delimiter(issues_count)
|
||||
- if group_sidebar_link?(:issues)
|
||||
= nav_link(path: issues_sub_menu_items) do
|
||||
= link_to issues_group_path(@group) do
|
||||
.nav-icon-container
|
||||
= sprite_icon('issues')
|
||||
%span.nav-item-name
|
||||
Issues
|
||||
%span.badge.count= number_with_delimiter(issues_count)
|
||||
|
||||
%ul.sidebar-sub-level-items
|
||||
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to issues_group_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Issues') }
|
||||
%span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
|
||||
%li.divider.fly-out-top-item
|
||||
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
|
||||
= link_to issues_group_path(@group), title: 'List' do
|
||||
%span
|
||||
List
|
||||
%ul.sidebar-sub-level-items
|
||||
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to issues_group_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Issues') }
|
||||
%span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
|
||||
%li.divider.fly-out-top-item
|
||||
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
|
||||
= link_to issues_group_path(@group), title: 'List' do
|
||||
%span
|
||||
List
|
||||
|
||||
= nav_link(path: 'labels#index') do
|
||||
= link_to group_labels_path(@group), title: 'Labels' do
|
||||
%span
|
||||
Labels
|
||||
- if group_sidebar_link?(:labels)
|
||||
= nav_link(path: 'labels#index') do
|
||||
= link_to group_labels_path(@group), title: 'Labels' do
|
||||
%span
|
||||
Labels
|
||||
|
||||
= nav_link(path: 'milestones#index') do
|
||||
= link_to group_milestones_path(@group), title: 'Milestones' do
|
||||
%span
|
||||
Milestones
|
||||
- if group_sidebar_link?(:milestones)
|
||||
= nav_link(path: 'milestones#index') do
|
||||
= link_to group_milestones_path(@group), title: 'Milestones' do
|
||||
%span
|
||||
Milestones
|
||||
|
||||
= nav_link(path: 'groups#merge_requests') do
|
||||
= link_to merge_requests_group_path(@group) do
|
||||
.nav-icon-container
|
||||
= sprite_icon('git-merge')
|
||||
%span.nav-item-name
|
||||
Merge Requests
|
||||
%span.badge.count= number_with_delimiter(merge_requests_count)
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to merge_requests_group_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Merge Requests') }
|
||||
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
|
||||
= nav_link(path: 'group_members#index') do
|
||||
= link_to group_group_members_path(@group) do
|
||||
.nav-icon-container
|
||||
= sprite_icon('users')
|
||||
%span.nav-item-name
|
||||
Members
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to group_group_members_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Members') }
|
||||
- if current_user && can?(current_user, :admin_group, @group)
|
||||
- if group_sidebar_link?(:merge_requests)
|
||||
= nav_link(path: 'groups#merge_requests') do
|
||||
= link_to merge_requests_group_path(@group) do
|
||||
.nav-icon-container
|
||||
= sprite_icon('git-merge')
|
||||
%span.nav-item-name
|
||||
Merge Requests
|
||||
%span.badge.count= number_with_delimiter(merge_requests_count)
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to merge_requests_group_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Merge Requests') }
|
||||
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
|
||||
|
||||
- if group_sidebar_link?(:group_members)
|
||||
= nav_link(path: 'group_members#index') do
|
||||
= link_to group_group_members_path(@group) do
|
||||
.nav-icon-container
|
||||
= sprite_icon('users')
|
||||
%span.nav-item-name
|
||||
Members
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to group_group_members_path(@group) do
|
||||
%strong.fly-out-top-item-name
|
||||
#{ _('Members') }
|
||||
|
||||
- if group_sidebar_link?(:settings)
|
||||
= nav_link(path: group_nav_link_paths) do
|
||||
= link_to edit_group_path(@group) do
|
||||
.nav-icon-container
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
- user = local_assigns[:user]
|
||||
- access = user&.max_member_access_for_project(project.id) unless user.nil?
|
||||
- css_class = '' unless local_assigns[:css_class]
|
||||
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
|
||||
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
|
||||
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
|
||||
- cache_key = project_list_cache_key(project)
|
||||
- updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
|
||||
|
@ -47,7 +47,7 @@
|
|||
.prepend-top-0
|
||||
- if project.archived
|
||||
%span.prepend-left-10.label.label-warning archived
|
||||
- if project.pipeline_status.has_status?
|
||||
- if can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
|
||||
%span.prepend-left-10
|
||||
= render_project_pipeline_status(project.pipeline_status)
|
||||
- if forks
|
||||
|
|
|
@ -82,47 +82,58 @@
|
|||
.fade-left= icon('angle-left')
|
||||
.fade-right= icon('angle-right')
|
||||
%ul.nav-links.user-profile-nav.scrolling-tabs
|
||||
%li.js-activity-tab
|
||||
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
|
||||
Activity
|
||||
%li.js-groups-tab
|
||||
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
|
||||
Groups
|
||||
%li.js-contributed-tab
|
||||
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
|
||||
Contributed projects
|
||||
%li.js-projects-tab
|
||||
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
|
||||
Personal projects
|
||||
%li.js-snippets-tab
|
||||
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
|
||||
Snippets
|
||||
- if profile_tab?(:activity)
|
||||
%li.js-activity-tab
|
||||
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
|
||||
Activity
|
||||
- if profile_tab?(:groups)
|
||||
%li.js-groups-tab
|
||||
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
|
||||
Groups
|
||||
- if profile_tab?(:contributed)
|
||||
%li.js-contributed-tab
|
||||
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
|
||||
Contributed projects
|
||||
- if profile_tab?(:projects)
|
||||
%li.js-projects-tab
|
||||
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
|
||||
Personal projects
|
||||
- if profile_tab?(:snippets)
|
||||
%li.js-snippets-tab
|
||||
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
|
||||
Snippets
|
||||
|
||||
%div{ class: container_class }
|
||||
.tab-content
|
||||
#activity.tab-pane
|
||||
.row-content-block.calender-block.white.second-block.hidden-xs
|
||||
.user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
|
||||
%h4.center.light
|
||||
%i.fa.fa-spinner.fa-spin
|
||||
.user-calendar-activities
|
||||
- if profile_tab?(:activity)
|
||||
#activity.tab-pane
|
||||
.row-content-block.calender-block.white.second-block.hidden-xs
|
||||
.user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
|
||||
%h4.center.light
|
||||
%i.fa.fa-spinner.fa-spin
|
||||
.user-calendar-activities
|
||||
|
||||
%h4.prepend-top-20
|
||||
Most Recent Activity
|
||||
.content_list{ data: { href: user_path } }
|
||||
= spinner
|
||||
- if can?(current_user, :read_cross_project)
|
||||
%h4.prepend-top-20
|
||||
Most Recent Activity
|
||||
.content_list{ data: { href: user_path } }
|
||||
= spinner
|
||||
|
||||
#groups.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
- if profile_tab?(:groups)
|
||||
#groups.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
|
||||
#contributed.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
- if profile_tab?(:contributed)
|
||||
#contributed.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
|
||||
#projects.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
- if profile_tab?(:projects)
|
||||
#projects.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
|
||||
#snippets.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
- if profile_tab?(:snippets)
|
||||
#snippets.tab-pane
|
||||
-# This tab is always loaded via AJAX
|
||||
|
||||
.loading-status
|
||||
= spinner
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Authorize project access with an external service
|
||||
merge_request: 4675
|
||||
author:
|
||||
type: added
|
25
config/initializers/0_as_concern.rb
Normal file
25
config/initializers/0_as_concern.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# This module is based on: https://gist.github.com/bcardarella/5735987
|
||||
|
||||
module Prependable
|
||||
def prepend_features(base)
|
||||
if base.instance_variable_defined?(:@_dependencies)
|
||||
base.instance_variable_get(:@_dependencies) << self
|
||||
false
|
||||
else
|
||||
return false if base < self
|
||||
|
||||
super
|
||||
base.singleton_class.send(:prepend, const_get('ClassMethods')) if const_defined?(:ClassMethods)
|
||||
@_dependencies.each { |dep| base.send(:prepend, dep) } # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block) # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveSupport
|
||||
module Concern
|
||||
prepend Prependable
|
||||
|
||||
alias_method :prepended, :included
|
||||
end
|
||||
end
|
|
@ -172,7 +172,7 @@ module API
|
|||
|
||||
def find_project_snippet(id)
|
||||
finder_params = { project: user_project }
|
||||
SnippetsFinder.new(current_user, finder_params).execute.find(id)
|
||||
SnippetsFinder.new(current_user, finder_params).find(id)
|
||||
end
|
||||
|
||||
def find_merge_request_with_access(iid, access_level = :read_merge_request)
|
||||
|
|
|
@ -147,7 +147,7 @@ module API
|
|||
attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled)
|
||||
end
|
||||
|
||||
if current_settings.update_attributes(attrs)
|
||||
if ApplicationSettings::UpdateService.new(current_settings, current_user, attrs).execute
|
||||
present current_settings, with: Entities::ApplicationSetting
|
||||
else
|
||||
render_validation_error!(current_settings)
|
||||
|
|
|
@ -15,6 +15,8 @@ module Banzai
|
|||
issuables = extractor.extract([doc])
|
||||
|
||||
issuables.each do |node, issuable|
|
||||
next if !can_read_cross_project? && issuable.project != project
|
||||
|
||||
if VISIBLE_STATES.include?(issuable.state) && node.inner_html == issuable.reference_link_text(project)
|
||||
node.content += " (#{issuable.state})"
|
||||
end
|
||||
|
@ -25,6 +27,10 @@ module Banzai
|
|||
|
||||
private
|
||||
|
||||
def can_read_cross_project?
|
||||
Ability.allowed?(current_user, :read_cross_project)
|
||||
end
|
||||
|
||||
def current_user
|
||||
context[:current_user]
|
||||
end
|
||||
|
|
|
@ -64,7 +64,7 @@ module Banzai
|
|||
finder_params[:group_ids] = [project.group.id]
|
||||
end
|
||||
|
||||
MilestonesFinder.new(finder_params).execute.find_by(params)
|
||||
MilestonesFinder.new(finder_params).find_by(params)
|
||||
end
|
||||
|
||||
def url_for_object(milestone, project)
|
||||
|
|
|
@ -19,8 +19,9 @@ module Banzai
|
|||
#
|
||||
# Returns the documents passed as the first argument.
|
||||
def redact(documents)
|
||||
all_document_nodes = document_nodes(documents)
|
||||
redact_cross_project_references(documents) unless can_read_cross_project?
|
||||
|
||||
all_document_nodes = document_nodes(documents)
|
||||
redact_document_nodes(all_document_nodes)
|
||||
end
|
||||
|
||||
|
@ -51,6 +52,18 @@ module Banzai
|
|||
metadata
|
||||
end
|
||||
|
||||
def redact_cross_project_references(documents)
|
||||
extractor = Banzai::IssuableExtractor.new(project, user)
|
||||
issuables = extractor.extract(documents)
|
||||
|
||||
issuables.each do |node, issuable|
|
||||
next if issuable.project == project
|
||||
|
||||
node['class'] = node['class'].gsub('has-tooltip', '')
|
||||
node['title'] = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the nodes visible to the current user.
|
||||
#
|
||||
# nodes - The input nodes to check.
|
||||
|
@ -78,5 +91,11 @@ module Banzai
|
|||
{ document: document, nodes: Querying.css(document, 'a.gfm[data-reference-type]') }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_read_cross_project?
|
||||
Ability.allowed?(user, :read_cross_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ module Banzai
|
|||
end
|
||||
|
||||
def can_read_reference?(user, issuable)
|
||||
can?(user, "read_#{issuable.class.to_s.underscore}".to_sym, issuable)
|
||||
can?(user, "read_#{issuable.class.to_s.underscore}_iid".to_sym, issuable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,12 +5,31 @@ module Banzai
|
|||
|
||||
def nodes_visible_to_user(user, nodes)
|
||||
issues = records_for_nodes(nodes)
|
||||
issues_to_check = issues.values
|
||||
|
||||
readable_issues = Ability
|
||||
.issues_readable_by_user(issues.values, user).to_set
|
||||
unless can?(user, :read_cross_project)
|
||||
issues_to_check, cross_project_issues = issues_to_check.partition do |issue|
|
||||
issue.project == project
|
||||
end
|
||||
end
|
||||
|
||||
readable_issues = Ability.issues_readable_by_user(issues_to_check, user).to_set
|
||||
|
||||
nodes.select do |node|
|
||||
readable_issues.include?(issues[node])
|
||||
issue_in_node = issues[node]
|
||||
|
||||
# We check the inclusion of readable issues first because it's faster.
|
||||
#
|
||||
# But we need to fall back to `read_issue_iid` if the user cannot read
|
||||
# cross project, since it might be possible the user can see the IID
|
||||
# but not the issue.
|
||||
if readable_issues.include?(issue_in_node)
|
||||
true
|
||||
elsif cross_project_issues&.include?(issue_in_node)
|
||||
can_read_reference?(user, issue_in_node)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def events_by_date(date)
|
||||
return Event.none unless can_read_cross_project?
|
||||
|
||||
events = Event.contributions.where(author_id: contributor.id)
|
||||
.where(created_at: date.beginning_of_day..date.end_of_day)
|
||||
.where(project_id: projects)
|
||||
|
@ -53,6 +55,10 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def can_read_cross_project?
|
||||
Ability.allowed?(current_user, :read_cross_project)
|
||||
end
|
||||
|
||||
def event_counts(date_from, feature)
|
||||
t = Event.arel_table
|
||||
|
||||
|
|
67
lib/gitlab/cross_project_access.rb
Normal file
67
lib/gitlab/cross_project_access.rb
Normal file
|
@ -0,0 +1,67 @@
|
|||
module Gitlab
|
||||
class CrossProjectAccess
|
||||
class << self
|
||||
delegate :add_check, :find_check, :checks,
|
||||
to: :instance
|
||||
end
|
||||
|
||||
def self.instance
|
||||
@instance ||= new
|
||||
end
|
||||
|
||||
attr_reader :checks
|
||||
|
||||
def initialize
|
||||
@checks = {}
|
||||
end
|
||||
|
||||
def add_check(
|
||||
klass,
|
||||
actions: {},
|
||||
positive_condition: nil,
|
||||
negative_condition: nil,
|
||||
skip: false)
|
||||
|
||||
new_check = CheckInfo.new(actions,
|
||||
positive_condition,
|
||||
negative_condition,
|
||||
skip
|
||||
)
|
||||
|
||||
@checks[klass] ||= Gitlab::CrossProjectAccess::CheckCollection.new
|
||||
@checks[klass].add_check(new_check)
|
||||
recalculate_checks_for_class(klass)
|
||||
|
||||
@checks[klass]
|
||||
end
|
||||
|
||||
def find_check(object)
|
||||
@cached_checks ||= Hash.new do |cache, new_class|
|
||||
parent_classes = @checks.keys.select { |existing_class| new_class <= existing_class }
|
||||
closest_class = closest_parent(parent_classes, new_class)
|
||||
cache[new_class] = @checks[closest_class]
|
||||
end
|
||||
|
||||
@cached_checks[object.class]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def recalculate_checks_for_class(klass)
|
||||
new_collection = @checks[klass]
|
||||
|
||||
@checks.each do |existing_class, existing_check_collection|
|
||||
if existing_class < klass
|
||||
existing_check_collection.add_collection(new_collection)
|
||||
elsif klass < existing_class
|
||||
new_collection.add_collection(existing_check_collection)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def closest_parent(classes, subject)
|
||||
relevant_ancestors = subject.ancestors & classes
|
||||
relevant_ancestors.first
|
||||
end
|
||||
end
|
||||
end
|
47
lib/gitlab/cross_project_access/check_collection.rb
Normal file
47
lib/gitlab/cross_project_access/check_collection.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
module Gitlab
|
||||
class CrossProjectAccess
|
||||
class CheckCollection
|
||||
attr_reader :checks
|
||||
|
||||
def initialize
|
||||
@checks = []
|
||||
end
|
||||
|
||||
def add_collection(collection)
|
||||
@checks |= collection.checks
|
||||
end
|
||||
|
||||
def add_check(check)
|
||||
@checks << check
|
||||
end
|
||||
|
||||
def should_run?(object)
|
||||
skips, runs = arranged_checks
|
||||
|
||||
# If one rule tells us to skip, we skip the cross project check
|
||||
return false if skips.any? { |check| check.should_skip?(object) }
|
||||
|
||||
# If the rule isn't skipped, we run it if any of the checks says we
|
||||
# should run
|
||||
runs.any? { |check| check.should_run?(object) }
|
||||
end
|
||||
|
||||
def arranged_checks
|
||||
return [@skips, @runs] if @skips && @runs
|
||||
|
||||
@skips = []
|
||||
@runs = []
|
||||
|
||||
@checks.each do |check|
|
||||
if check.skip
|
||||
@skips << check
|
||||
else
|
||||
@runs << check
|
||||
end
|
||||
end
|
||||
|
||||
[@skips, @runs]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
66
lib/gitlab/cross_project_access/check_info.rb
Normal file
66
lib/gitlab/cross_project_access/check_info.rb
Normal file
|
@ -0,0 +1,66 @@
|
|||
module Gitlab
|
||||
class CrossProjectAccess
|
||||
class CheckInfo
|
||||
attr_accessor :actions, :positive_condition, :negative_condition, :skip
|
||||
|
||||
def initialize(actions, positive_condition, negative_condition, skip)
|
||||
@actions = actions
|
||||
@positive_condition = positive_condition
|
||||
@negative_condition = negative_condition
|
||||
@skip = skip
|
||||
end
|
||||
|
||||
def should_skip?(object)
|
||||
return !should_run?(object) unless @skip
|
||||
|
||||
skip_for_action = @actions[current_action(object)]
|
||||
skip_for_action = false if @actions[current_action(object)].nil?
|
||||
|
||||
# We need to do the opposite of what was defined in the following cases:
|
||||
# - skip_cross_project_access_check index: true, if: -> { false }
|
||||
# - skip_cross_project_access_check index: true, unless: -> { true }
|
||||
if positive_condition_is_false?(object)
|
||||
skip_for_action = !skip_for_action
|
||||
end
|
||||
|
||||
if negative_condition_is_true?(object)
|
||||
skip_for_action = !skip_for_action
|
||||
end
|
||||
|
||||
skip_for_action
|
||||
end
|
||||
|
||||
def should_run?(object)
|
||||
return !should_skip?(object) if @skip
|
||||
|
||||
run_for_action = @actions[current_action(object)]
|
||||
run_for_action = true if @actions[current_action(object)].nil?
|
||||
|
||||
# We need to do the opposite of what was defined in the following cases:
|
||||
# - requires_cross_project_access index: true, if: -> { false }
|
||||
# - requires_cross_project_access index: true, unless: -> { true }
|
||||
if positive_condition_is_false?(object)
|
||||
run_for_action = !run_for_action
|
||||
end
|
||||
|
||||
if negative_condition_is_true?(object)
|
||||
run_for_action = !run_for_action
|
||||
end
|
||||
|
||||
run_for_action
|
||||
end
|
||||
|
||||
def positive_condition_is_false?(object)
|
||||
@positive_condition && !object.instance_exec(&@positive_condition)
|
||||
end
|
||||
|
||||
def negative_condition_is_true?(object)
|
||||
@negative_condition && object.instance_exec(&@negative_condition)
|
||||
end
|
||||
|
||||
def current_action(object)
|
||||
object.respond_to?(:action_name) ? object.action_name.to_sym : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
48
lib/gitlab/cross_project_access/class_methods.rb
Normal file
48
lib/gitlab/cross_project_access/class_methods.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
module Gitlab
|
||||
class CrossProjectAccess
|
||||
module ClassMethods
|
||||
def requires_cross_project_access(*args)
|
||||
positive_condition, negative_condition, actions = extract_params(args)
|
||||
|
||||
Gitlab::CrossProjectAccess.add_check(
|
||||
self,
|
||||
actions: actions,
|
||||
positive_condition: positive_condition,
|
||||
negative_condition: negative_condition
|
||||
)
|
||||
end
|
||||
|
||||
def skip_cross_project_access_check(*args)
|
||||
positive_condition, negative_condition, actions = extract_params(args)
|
||||
|
||||
Gitlab::CrossProjectAccess.add_check(
|
||||
self,
|
||||
actions: actions,
|
||||
positive_condition: positive_condition,
|
||||
negative_condition: negative_condition,
|
||||
skip: true
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_params(args)
|
||||
actions = {}
|
||||
positive_condition = nil
|
||||
negative_condition = nil
|
||||
|
||||
args.each do |argument|
|
||||
if argument.is_a?(Hash)
|
||||
positive_condition = argument.delete(:if)
|
||||
negative_condition = argument.delete(:unless)
|
||||
actions.merge!(argument)
|
||||
else
|
||||
actions[argument] = true
|
||||
end
|
||||
end
|
||||
|
||||
[positive_condition, negative_condition, actions]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -65,7 +65,7 @@ module Gitlab
|
|||
return false unless can_access_git?
|
||||
|
||||
if protected?(ProtectedBranch, project, ref)
|
||||
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
|
||||
return true if project.user_can_push_to_empty_repo?(user)
|
||||
|
||||
protected_branch_accessible_to?(ref, action: :push)
|
||||
else
|
||||
|
|
|
@ -8,8 +8,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-02-07 11:38-0600\n"
|
||||
"PO-Revision-Date: 2018-02-07 11:38-0600\n"
|
||||
"POT-Creation-Date: 2018-02-20 10:26+0100\n"
|
||||
"PO-Revision-Date: 2018-02-20 10:26+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
@ -150,6 +150,39 @@ msgstr ""
|
|||
msgid "AdminHealthPageLink|health page"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminProjects|Delete"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminProjects|Delete Project %{projectName}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminProjects|Delete project"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Block user"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Delete User %{username} and contributions?"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Delete User %{username}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Delete user"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|Delete user and contributions"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|To confirm, type %{projectName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminUsers|To confirm, type %{username}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Advanced settings"
|
||||
msgstr ""
|
||||
|
||||
|
@ -177,9 +210,21 @@ msgstr ""
|
|||
msgid "An error occurred while getting projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while importing project"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while loading commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while loading diff"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while loading filenames"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while loading the file"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while rendering KaTeX"
|
||||
msgstr ""
|
||||
|
||||
|
@ -192,6 +237,9 @@ msgstr ""
|
|||
msgid "An error occurred while retrieving diff"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while saving assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while validating username"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1018,6 +1066,9 @@ msgstr ""
|
|||
msgid "Create a personal access token on your account to pull or push via %{protocol}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Create branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create directory"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1033,6 +1084,9 @@ msgstr ""
|
|||
msgid "Create merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create merge request and branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create new branch"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1290,9 +1344,15 @@ msgstr ""
|
|||
msgid "Failed to change the owner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to remove issue from board, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to remove the pipeline schedule"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to update issues, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Feb"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1985,6 +2045,24 @@ msgstr ""
|
|||
msgid "Pipelines|Get started with Pipelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Retry pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Retry pipeline #%{id}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Stop pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Stop pipeline #%{id}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|You’re about to retry pipeline %{id}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|You’re about to stop pipeline %{id}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|all"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2144,12 +2222,30 @@ msgstr ""
|
|||
msgid "ProjectsDropdown|This feature requires browser localStorage support"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Auto configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Finding and configuring metrics..."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Install Prometheus on clusters"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Manage clusters"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Manual configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Metrics"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2171,9 +2267,18 @@ msgstr ""
|
|||
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Time-series monitoring service"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|View environments"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2376,12 +2481,18 @@ msgstr ""
|
|||
msgid "Something went wrong when toggling the button"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while closing the issue. Please try again later"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while fetching the projects."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while fetching the registry list."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while reopening the issue. Please try again later"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -2478,6 +2589,9 @@ msgstr ""
|
|||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source (branch or tag)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source code"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2738,6 +2852,9 @@ msgstr ""
|
|||
msgid "This merge request is locked."
|
||||
msgstr ""
|
||||
|
||||
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
|
||||
msgstr ""
|
||||
|
||||
msgid "This project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2934,9 +3051,6 @@ msgstr ""
|
|||
msgid "Trigger this manual action"
|
||||
msgstr ""
|
||||
|
||||
msgid "Type %{value} to confirm:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to reset project cache."
|
||||
msgstr ""
|
||||
|
||||
|
@ -3229,6 +3343,9 @@ msgid_plural "merge requests"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Cancel automatic merge"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3262,6 +3379,9 @@ msgstr ""
|
|||
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Mentions"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3349,6 +3469,9 @@ msgstr ""
|
|||
msgid "mrWidget|You can remove source branch now"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|branch does not exist."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|command line"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ describe Boards::IssuesController do
|
|||
|
||||
context 'with unauthorized user' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ControllerWithCrossProjectAccessCheck do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
render_views
|
||||
|
||||
context 'When reading cross project is not allowed' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed).and_call_original
|
||||
allow(Ability).to receive(:allowed?)
|
||||
.with(user, :read_cross_project, :global)
|
||||
.and_return(false)
|
||||
end
|
||||
|
||||
describe '#requires_cross_project_access' do
|
||||
controller(ApplicationController) do
|
||||
# `described_class` is not available in this context
|
||||
include ControllerWithCrossProjectAccessCheck # rubocop:disable RSpec/DescribedClass
|
||||
|
||||
requires_cross_project_access :index, show: false,
|
||||
unless: -> { unless_condition },
|
||||
if: -> { if_condition }
|
||||
|
||||
def index
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def show
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def unless_condition
|
||||
false
|
||||
end
|
||||
|
||||
def if_condition
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders a 404 with trying to access a cross project page' do
|
||||
message = "This page is unavailable because you are not allowed to read "\
|
||||
"information across multiple projects."
|
||||
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
expect(response.body).to match(/#{message}/)
|
||||
end
|
||||
|
||||
it 'is skipped when the `if` condition returns false' do
|
||||
expect(controller).to receive(:if_condition).and_return(false)
|
||||
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'is skipped when the `unless` condition returns true' do
|
||||
expect(controller).to receive(:unless_condition).and_return(true)
|
||||
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'correctly renders an action that does not require cross project access' do
|
||||
get :show, id: 'nothing'
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#skip_cross_project_access_check' do
|
||||
controller(ApplicationController) do
|
||||
# `described_class` is not available in this context
|
||||
include ControllerWithCrossProjectAccessCheck # rubocop:disable RSpec/DescribedClass
|
||||
|
||||
requires_cross_project_access
|
||||
|
||||
skip_cross_project_access_check index: true, show: false,
|
||||
unless: -> { unless_condition },
|
||||
if: -> { if_condition }
|
||||
|
||||
def index
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def show
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def edit
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def unless_condition
|
||||
false
|
||||
end
|
||||
|
||||
def if_condition
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders a success when the check is skipped' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'is executed when the `if` condition returns false' do
|
||||
expect(controller).to receive(:if_condition).and_return(false)
|
||||
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
|
||||
it 'is executed when the `unless` condition returns true' do
|
||||
expect(controller).to receive(:unless_condition).and_return(true)
|
||||
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
|
||||
it 'does not skip the check on an action that is not skipped' do
|
||||
get :show, id: 'hello'
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
|
||||
it 'does not skip the check on an action that was not defined to skip' do
|
||||
get :edit, id: 'hello'
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,7 +17,7 @@ describe Projects::MergeRequests::CreationsController do
|
|||
|
||||
before do
|
||||
fork_project.add_master(user)
|
||||
|
||||
Projects::ForkService.new(project, user).execute(fork_project)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
@ -125,4 +125,66 @@ describe Projects::MergeRequests::CreationsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #branch_to' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
end
|
||||
|
||||
it 'fetches the commit if a user has access' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
|
||||
|
||||
get :branch_to,
|
||||
namespace_id: fork_project.namespace,
|
||||
project_id: fork_project,
|
||||
target_project_id: project.id,
|
||||
ref: 'master'
|
||||
|
||||
expect(assigns(:commit)).not_to be_nil
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'does not load the commit when the user cannot read the project' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false }
|
||||
|
||||
get :branch_to,
|
||||
namespace_id: fork_project.namespace,
|
||||
project_id: fork_project,
|
||||
target_project_id: project.id,
|
||||
ref: 'master'
|
||||
|
||||
expect(assigns(:commit)).to be_nil
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #update_branches' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
end
|
||||
|
||||
it 'lists the branches of another fork if the user has access' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
|
||||
|
||||
get :update_branches,
|
||||
namespace_id: fork_project.namespace,
|
||||
project_id: fork_project,
|
||||
target_project_id: project.id
|
||||
|
||||
expect(assigns(:target_branches)).not_to be_empty
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'does not list branches when the user cannot read the project' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false }
|
||||
|
||||
get :update_branches,
|
||||
namespace_id: fork_project.namespace,
|
||||
project_id: fork_project,
|
||||
target_project_id: project.id
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(assigns(:target_branches)).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,32 @@ describe SearchController do
|
|||
expect(assigns[:search_objects].first).to eq note
|
||||
end
|
||||
|
||||
context 'when the user cannot read cross project' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?)
|
||||
.with(user, :read_cross_project, :global) { false }
|
||||
end
|
||||
|
||||
it 'still allows accessing the search page' do
|
||||
get :show
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'still blocks searches without a project_id' do
|
||||
get :show, search: 'hello'
|
||||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
|
||||
it 'allows searches with a project_id' do
|
||||
get :show, search: 'hello', project_id: create(:project, :public).id
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'on restricted projects' do
|
||||
context 'when signed out' do
|
||||
before do
|
||||
|
|
|
@ -74,6 +74,31 @@ describe UsersController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'json with events' do
|
||||
let(:project) { create(:project) }
|
||||
before do
|
||||
project.add_developer(user)
|
||||
Gitlab::DataBuilder::Push.build_sample(project, user)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'loads events' do
|
||||
get :show, username: user, format: :json
|
||||
|
||||
expect(assigns(:events)).not_to be_empty
|
||||
end
|
||||
|
||||
it 'hides events if the user cannot read cross project' do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
|
||||
|
||||
get :show, username: user, format: :json
|
||||
|
||||
expect(assigns(:events)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #calendar' do
|
||||
|
|
17
spec/features/users/show_spec.rb
Normal file
17
spec/features/users/show_spec.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'User page' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'shows all the tabs' do
|
||||
visit(user_path(user))
|
||||
|
||||
page.within '.nav-links' do
|
||||
expect(page).to have_link('Activity')
|
||||
expect(page).to have_link('Groups')
|
||||
expect(page).to have_link('Contributed projects')
|
||||
expect(page).to have_link('Personal projects')
|
||||
expect(page).to have_link('Snippets')
|
||||
end
|
||||
end
|
||||
end
|
70
spec/finders/concerns/finder_methods_spec.rb
Normal file
70
spec/finders/concerns/finder_methods_spec.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe FinderMethods do
|
||||
let(:finder_class) do
|
||||
Class.new do
|
||||
include FinderMethods
|
||||
|
||||
attr_reader :current_user
|
||||
|
||||
def initialize(user)
|
||||
@current_user = user
|
||||
end
|
||||
|
||||
def execute
|
||||
Project.all
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:finder) { finder_class.new(user) }
|
||||
let(:authorized_project) { create(:project) }
|
||||
let(:unauthorized_project) { create(:project) }
|
||||
|
||||
before do
|
||||
authorized_project.add_developer(user)
|
||||
end
|
||||
|
||||
describe '#find_by!' do
|
||||
it 'returns the project if the user has access' do
|
||||
expect(finder.find_by!(id: authorized_project.id)).to eq(authorized_project)
|
||||
end
|
||||
|
||||
it 'raises not found when the project is not found' do
|
||||
expect { finder.find_by!(id: 0) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'raises not found the user does not have access' do
|
||||
expect { finder.find_by!(id: unauthorized_project.id) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find' do
|
||||
it 'returns the project if the user has access' do
|
||||
expect(finder.find(authorized_project.id)).to eq(authorized_project)
|
||||
end
|
||||
|
||||
it 'raises not found when the project is not found' do
|
||||
expect { finder.find(0) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'raises not found the user does not have access' do
|
||||
expect { finder.find(unauthorized_project.id) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_by' do
|
||||
it 'returns the project if the user has access' do
|
||||
expect(finder.find_by(id: authorized_project.id)).to eq(authorized_project)
|
||||
end
|
||||
|
||||
it 'returns nil when the project is not found' do
|
||||
expect(finder.find_by(id: 0)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil when the user does not have access' do
|
||||
expect(finder.find_by(id: unauthorized_project.id)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
118
spec/finders/concerns/finder_with_cross_project_access_spec.rb
Normal file
118
spec/finders/concerns/finder_with_cross_project_access_spec.rb
Normal file
|
@ -0,0 +1,118 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe FinderWithCrossProjectAccess do
|
||||
let(:finder_class) do
|
||||
Class.new do
|
||||
prepend FinderWithCrossProjectAccess
|
||||
include FinderMethods
|
||||
|
||||
requires_cross_project_access if: -> { requires_access? }
|
||||
|
||||
attr_reader :current_user
|
||||
|
||||
def initialize(user)
|
||||
@current_user = user
|
||||
end
|
||||
|
||||
def execute
|
||||
Issue.all
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:user) { create(:user) }
|
||||
subject(:finder) { finder_class.new(user) }
|
||||
let!(:result) { create(:issue) }
|
||||
|
||||
before do
|
||||
result.project.add_master(user)
|
||||
end
|
||||
|
||||
def expect_access_check_on_result
|
||||
expect(finder).not_to receive(:requires_access?)
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_issue, result).and_call_original
|
||||
end
|
||||
|
||||
context 'when the user cannot read cross project' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_cross_project)
|
||||
.and_return(false)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
it 'returns a issue if the check is disabled' do
|
||||
expect(finder).to receive(:requires_access?).and_return(false)
|
||||
|
||||
expect(finder.execute).to include(result)
|
||||
end
|
||||
|
||||
it 'returns an empty relation when the check is enabled' do
|
||||
expect(finder).to receive(:requires_access?).and_return(true)
|
||||
|
||||
expect(finder.execute).to be_empty
|
||||
end
|
||||
|
||||
it 'only queries once when check is enabled' do
|
||||
expect(finder).to receive(:requires_access?).and_return(true)
|
||||
|
||||
expect { finder.execute }.not_to exceed_query_limit(1)
|
||||
end
|
||||
|
||||
it 'only queries once when check is disabled' do
|
||||
expect(finder).to receive(:requires_access?).and_return(false)
|
||||
|
||||
expect { finder.execute }.not_to exceed_query_limit(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find' do
|
||||
it 'checks the accessibility of the subject directly' do
|
||||
expect_access_check_on_result
|
||||
|
||||
finder.find(result.id)
|
||||
end
|
||||
|
||||
it 'returns the issue' do
|
||||
expect(finder.find(result.id)).to eq(result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_by' do
|
||||
it 'checks the accessibility of the subject directly' do
|
||||
expect_access_check_on_result
|
||||
|
||||
finder.find_by(id: result.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_by!' do
|
||||
it 'checks the accessibility of the subject directly' do
|
||||
expect_access_check_on_result
|
||||
|
||||
finder.find_by!(id: result.id)
|
||||
end
|
||||
|
||||
it 're-enables the check after the find failed' do
|
||||
finder.find_by!(id: 9999) rescue ActiveRecord::RecordNotFound
|
||||
|
||||
expect(finder.instance_variable_get(:@should_skip_cross_project_check))
|
||||
.to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user can read cross project' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_cross_project)
|
||||
.and_return(true)
|
||||
end
|
||||
|
||||
it 'returns the result' do
|
||||
expect(finder).not_to receive(:requires_access?)
|
||||
|
||||
expect(finder.execute).to include(result)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,6 +26,14 @@ describe EventsFinder do
|
|||
|
||||
expect(events).not_to include(opened_merge_request_event)
|
||||
end
|
||||
|
||||
it 'returns nothing when the current user cannot read cross project' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
|
||||
|
||||
events = described_class.new(source: user, current_user: user).execute
|
||||
|
||||
expect(events).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when targeting a project' do
|
||||
|
|
|
@ -70,4 +70,12 @@ describe MilestonesFinder do
|
|||
expect(result.to_a).to contain_exactly(milestone_1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_by' do
|
||||
it 'finds a single milestone' do
|
||||
finder = described_class.new(project_ids: [project_1.id], state: 'all')
|
||||
|
||||
expect(finder.find_by(iid: milestone_3.iid)).to eq(milestone_3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -162,8 +162,26 @@ describe SnippetsFinder do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#execute" do
|
||||
# Snippet visibility scenarios are included in more details in spec/support/snippet_visibility.rb
|
||||
include_examples 'snippet visibility', described_class
|
||||
describe '#execute' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let!(:project_snippet) { create(:project_snippet, :public, project: project) }
|
||||
let!(:personal_snippet) { create(:personal_snippet, :public) }
|
||||
let(:user) { create(:user) }
|
||||
subject(:finder) { described_class.new(user) }
|
||||
|
||||
it 'returns project- and personal snippets' do
|
||||
expect(finder.execute).to contain_exactly(project_snippet, personal_snippet)
|
||||
end
|
||||
|
||||
context 'when the user cannot read cross project' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
|
||||
end
|
||||
|
||||
it 'returns only personal snippets when the user cannot read cross project' do
|
||||
expect(finder.execute).to contain_exactly(personal_snippet)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
31
spec/finders/user_recent_events_finder_spec.rb
Normal file
31
spec/finders/user_recent_events_finder_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe UserRecentEventsFinder do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:project_owner) { project.creator }
|
||||
let!(:event) { create(:event, project: project, author: project_owner) }
|
||||
|
||||
subject(:finder) { described_class.new(user, project_owner) }
|
||||
|
||||
describe '#execute' do
|
||||
it 'does not include the event when a user does not have access to the project' do
|
||||
expect(finder.execute).to be_empty
|
||||
end
|
||||
|
||||
context 'when the user has access to a project' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'includes the event' do
|
||||
expect(finder.execute).to include(event)
|
||||
end
|
||||
|
||||
it 'does not include the event if the user cannot read cross project' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
|
||||
expect(finder.execute).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
24
spec/helpers/dashboard_helper_spec.rb
Normal file
24
spec/helpers/dashboard_helper_spec.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe DashboardHelper do
|
||||
let(:user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
allow(helper).to receive(:can?) { true }
|
||||
end
|
||||
|
||||
describe '#dashboard_nav_links' do
|
||||
it 'has all the expected links by default' do
|
||||
menu_items = [:projects, :groups, :activity, :milestones, :snippets]
|
||||
|
||||
expect(helper.dashboard_nav_links).to contain_exactly(*menu_items)
|
||||
end
|
||||
|
||||
it 'does not contain cross project elements when the user cannot read cross project' do
|
||||
expect(helper).to receive(:can?).with(user, :read_cross_project) { false }
|
||||
|
||||
expect(helper.dashboard_nav_links).not_to include(:activity, :milestones)
|
||||
end
|
||||
end
|
||||
end
|
18
spec/helpers/explore_helper_spec.rb
Normal file
18
spec/helpers/explore_helper_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ExploreHelper do
|
||||
let(:user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
allow(helper).to receive(:can?) { true }
|
||||
end
|
||||
|
||||
describe '#explore_nav_links' do
|
||||
it 'has all the expected links by default' do
|
||||
menu_items = [:projects, :groups, :snippets]
|
||||
|
||||
expect(helper.explore_nav_links).to contain_exactly(*menu_items)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -201,4 +201,39 @@ describe GroupsHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#group_sidebar_links' do
|
||||
let(:group) { create(:group, :public) }
|
||||
let(:user) { create(:user) }
|
||||
before do
|
||||
allow(helper).to receive(:current_user) { user }
|
||||
allow(helper).to receive(:can?) { true }
|
||||
helper.instance_variable_set(:@group, group)
|
||||
end
|
||||
|
||||
it 'returns all the expected links' do
|
||||
links = [
|
||||
:overview, :activity, :issues, :labels, :milestones, :merge_requests,
|
||||
:group_members, :settings
|
||||
]
|
||||
|
||||
expect(helper.group_sidebar_links).to include(*links)
|
||||
end
|
||||
|
||||
it 'includes settings when the user can admin the group' do
|
||||
expect(helper).to receive(:current_user) { user }
|
||||
expect(helper).to receive(:can?).with(user, :admin_group, group) { false }
|
||||
|
||||
expect(helper.group_sidebar_links).not_to include(:settings)
|
||||
end
|
||||
|
||||
it 'excludes cross project features when the user cannot read cross project' do
|
||||
cross_project_features = [:activity, :issues, :labels, :milestones,
|
||||
:merge_requests]
|
||||
|
||||
expect(helper).to receive(:can?).with(user, :read_cross_project) { false }
|
||||
|
||||
expect(helper.group_sidebar_links).not_to include(*cross_project_features)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -113,21 +113,6 @@ describe IssuesHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe "milestone_options" do
|
||||
it "gets closed milestone from current issue" do
|
||||
closed_milestone = create(:closed_milestone, project: project)
|
||||
milestone1 = create(:milestone, project: project)
|
||||
milestone2 = create(:milestone, project: project)
|
||||
issue.update_attributes(milestone_id: closed_milestone.id)
|
||||
|
||||
options = milestone_options(issue)
|
||||
|
||||
expect(options).to have_selector('option[selected]', text: closed_milestone.title)
|
||||
expect(options).to have_selector('option', text: milestone1.title)
|
||||
expect(options).to have_selector('option', text: milestone2.title)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#link_to_discussions_to_resolve" do
|
||||
describe "passing only a merge request" do
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
|
|
53
spec/helpers/nav_helper_spec.rb
Normal file
53
spec/helpers/nav_helper_spec.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe NavHelper do
|
||||
describe '#header_links' do
|
||||
before do
|
||||
allow(helper).to receive(:session) { {} }
|
||||
end
|
||||
|
||||
context 'when the user is logged in' do
|
||||
let(:user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
allow(helper).to receive(:can?) { true }
|
||||
end
|
||||
|
||||
it 'has all the expected links by default' do
|
||||
menu_items = [:user_dropdown, :search, :issues, :merge_requests, :todos]
|
||||
|
||||
expect(helper.header_links).to contain_exactly(*menu_items)
|
||||
end
|
||||
|
||||
it 'contains the impersonation link while impersonating' do
|
||||
expect(helper).to receive(:session) { { impersonator_id: 1 } }
|
||||
|
||||
expect(helper.header_links).to include(:admin_impersonation)
|
||||
end
|
||||
|
||||
context 'when the user cannot read cross project' do
|
||||
before do
|
||||
allow(helper).to receive(:can?).with(user, :read_cross_project) { false }
|
||||
end
|
||||
|
||||
it 'does not contain cross project elements when the user cannot read cross project' do
|
||||
expect(helper.header_links).not_to include(:issues, :merge_requests, :todos, :search)
|
||||
end
|
||||
|
||||
it 'shows the search box when the user cannot read cross project and he is visiting a project' do
|
||||
helper.instance_variable_set(:@project, create(:project))
|
||||
|
||||
expect(helper.header_links).to include(:search)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns only the sign in and search when the user is not logged in' do
|
||||
allow(helper).to receive(:current_user).and_return(nil)
|
||||
allow(helper).to receive(:can?).with(nil, :read_cross_project) { true }
|
||||
|
||||
expect(helper.header_links).to contain_exactly(:sign_in, :search)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -75,6 +75,12 @@ describe ProjectsHelper do
|
|||
|
||||
describe "#project_list_cache_key", :clean_gitlab_redis_shared_state do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
allow(helper).to receive(:can?).with(user, :read_cross_project) { true }
|
||||
end
|
||||
|
||||
it "includes the route" do
|
||||
expect(helper.project_list_cache_key(project)).to include(project.route.cache_key)
|
||||
|
@ -106,6 +112,10 @@ describe ProjectsHelper do
|
|||
expect(helper.project_list_cache_key(project).last).to start_with('v')
|
||||
end
|
||||
|
||||
it 'includes wether or not the user can read cross project' do
|
||||
expect(helper.project_list_cache_key(project)).to include('cross-project:true')
|
||||
end
|
||||
|
||||
it "includes the pipeline status when there is a status" do
|
||||
create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
|
||||
|
||||
|
|
|
@ -14,4 +14,17 @@ describe UsersHelper do
|
|||
is_expected.to include("title=\"#{user.email}\"")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#profile_tabs' do
|
||||
subject(:tabs) { helper.profile_tabs }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
allow(helper).to receive(:can?).and_return(true)
|
||||
end
|
||||
|
||||
it 'includes all the expected tabs' do
|
||||
expect(tabs).to include(:activity, :groups, :contributed, :projects, :snippets)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ require 'spec_helper'
|
|||
describe Banzai::CommitRenderer do
|
||||
describe '.render' do
|
||||
it 'renders a commit description and title' do
|
||||
user = double(:user)
|
||||
user = build(:user)
|
||||
project = create(:project, :repository)
|
||||
|
||||
expect(Banzai::ObjectRenderer).to receive(:new).with(project, user).and_call_original
|
||||
|
|
|
@ -77,6 +77,14 @@ describe Banzai::Filter::IssuableStateFilter do
|
|||
expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)")
|
||||
end
|
||||
|
||||
it 'skips cross project references if the user cannot read cross project' do
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
|
||||
link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue')
|
||||
doc = filter(link, context.merge(project: other_project))
|
||||
|
||||
expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)}")
|
||||
end
|
||||
|
||||
it 'does not append state when filter is not enabled' do
|
||||
link = create_link('text', issue: closed_issue.id, reference_type: 'issue')
|
||||
context = { current_user: user }
|
||||
|
|
|
@ -6,7 +6,7 @@ describe Banzai::Filter::RedactorFilter do
|
|||
|
||||
it 'ignores non-GFM links' do
|
||||
html = %(See <a href="https://google.com/">Google</a>)
|
||||
doc = filter(html, current_user: double)
|
||||
doc = filter(html, current_user: build(:user))
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::Redactor do
|
||||
let(:user) { build(:user) }
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { build(:project) }
|
||||
let(:redactor) { described_class.new(project, user) }
|
||||
|
||||
|
@ -88,6 +88,55 @@ describe Banzai::Redactor do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the user cannot read cross project' do
|
||||
include ActionView::Helpers::UrlHelper
|
||||
let(:project) { create(:project) }
|
||||
let(:other_project) { create(:project, :public) }
|
||||
|
||||
def create_link(issuable)
|
||||
type = issuable.class.name.underscore.downcase
|
||||
link_to(issuable.to_reference, '',
|
||||
class: 'gfm has-tooltip',
|
||||
title: issuable.title,
|
||||
data: {
|
||||
reference_type: type,
|
||||
"#{type}": issuable.id
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global) { false }
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
|
||||
end
|
||||
|
||||
it 'skips links to issues within the same project' do
|
||||
issue = create(:issue, project: project)
|
||||
link = create_link(issue)
|
||||
doc = Nokogiri::HTML.fragment(link)
|
||||
|
||||
redactor.redact([doc])
|
||||
result = doc.css('a').last
|
||||
|
||||
expect(result['class']).to include('has-tooltip')
|
||||
expect(result['title']).to eq(issue.title)
|
||||
end
|
||||
|
||||
it 'removes info from a cross project reference' do
|
||||
issue = create(:issue, project: other_project)
|
||||
link = create_link(issue)
|
||||
doc = Nokogiri::HTML.fragment(link)
|
||||
|
||||
redactor.redact([doc])
|
||||
result = doc.css('a').last
|
||||
|
||||
expect(result['class']).not_to include('has-tooltip')
|
||||
expect(result['title']).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#redact_nodes' do
|
||||
it 'redacts an Array of nodes' do
|
||||
doc = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
|
||||
|
|
|
@ -19,19 +19,58 @@ describe Banzai::ReferenceParser::IssueParser do
|
|||
|
||||
it 'returns the nodes when the user can read the issue' do
|
||||
expect(Ability).to receive(:issues_readable_by_user)
|
||||
.with([issue], user)
|
||||
.and_return([issue])
|
||||
.with([issue], user)
|
||||
.and_return([issue])
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the user can not read the issue' do
|
||||
expect(Ability).to receive(:issues_readable_by_user)
|
||||
.with([issue], user)
|
||||
.and_return([])
|
||||
.with([issue], user)
|
||||
.and_return([])
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
|
||||
context 'when the user cannot read cross project' do
|
||||
let(:issue) { create(:issue) }
|
||||
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
|
||||
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global) { false }
|
||||
end
|
||||
|
||||
it 'returns the nodes when the user can read the issue' do
|
||||
expect(Ability).to receive(:allowed?)
|
||||
.with(user, :read_issue_iid, issue)
|
||||
.and_return(true)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the user can not read the issue' do
|
||||
expect(Ability).to receive(:allowed?)
|
||||
.with(user, :read_issue_iid, issue)
|
||||
.and_return(false)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
|
||||
context 'when the issue is not cross project' do
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
it 'does not check `can_read_reference` if the issue is not cross project' do
|
||||
expect(Ability).to receive(:issues_readable_by_user)
|
||||
.with([issue], user)
|
||||
.and_return([])
|
||||
|
||||
expect(subject).not_to receive(:can_read_reference?).with(user, issue)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-issue attribute' do
|
||||
|
|
|
@ -118,6 +118,19 @@ describe Gitlab::ContributionsCalendar do
|
|||
expect(calendar.events_by_date(today)).to contain_exactly(e1)
|
||||
expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3)
|
||||
end
|
||||
|
||||
context 'when the user cannot read read cross project' do
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
|
||||
end
|
||||
|
||||
it 'does not return any events' do
|
||||
create_event(public_project, today)
|
||||
|
||||
expect(calendar(user).events_by_date(today)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#starting_year' do
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::CrossProjectAccess::CheckCollection do
|
||||
subject(:collection) { described_class.new }
|
||||
|
||||
describe '#add_collection' do
|
||||
it 'merges the checks of 2 collections' do
|
||||
initial_check = double('check')
|
||||
collection.add_check(initial_check)
|
||||
|
||||
other_collection = described_class.new
|
||||
other_check = double('other_check')
|
||||
other_collection.add_check(other_check)
|
||||
|
||||
shared_check = double('shared check')
|
||||
other_collection.add_check(shared_check)
|
||||
collection.add_check(shared_check)
|
||||
|
||||
collection.add_collection(other_collection)
|
||||
|
||||
expect(collection.checks).to contain_exactly(initial_check, shared_check, other_check)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#should_run?' do
|
||||
def fake_check(run, skip)
|
||||
check = double("Check: run=#{run} - skip={skip}")
|
||||
allow(check).to receive(:should_run?).and_return(run)
|
||||
allow(check).to receive(:should_skip?).and_return(skip)
|
||||
allow(check).to receive(:skip).and_return(skip)
|
||||
|
||||
check
|
||||
end
|
||||
|
||||
it 'returns true if one of the check says it should run' do
|
||||
check = fake_check(true, false)
|
||||
other_check = fake_check(false, false)
|
||||
|
||||
collection.add_check(check)
|
||||
collection.add_check(other_check)
|
||||
|
||||
expect(collection.should_run?(double)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if one of the check says it should be skipped' do
|
||||
check = fake_check(true, false)
|
||||
other_check = fake_check(false, true)
|
||||
|
||||
collection.add_check(check)
|
||||
collection.add_check(other_check)
|
||||
|
||||
expect(collection.should_run?(double)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
111
spec/lib/gitlab/cross_project_access/check_info_spec.rb
Normal file
111
spec/lib/gitlab/cross_project_access/check_info_spec.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::CrossProjectAccess::CheckInfo do
|
||||
let(:dummy_controller) { double }
|
||||
|
||||
before do
|
||||
allow(dummy_controller).to receive(:action_name).and_return('index')
|
||||
end
|
||||
|
||||
describe '#should_run?' do
|
||||
it 'runs when an action is defined' do
|
||||
info = described_class.new({ index: true }, nil, nil, false)
|
||||
|
||||
expect(info.should_run?(dummy_controller)).to be_truthy
|
||||
end
|
||||
|
||||
it 'runs when the action is missing' do
|
||||
info = described_class.new({}, nil, nil, false)
|
||||
|
||||
expect(info.should_run?(dummy_controller)).to be_truthy
|
||||
end
|
||||
|
||||
it 'does not run when the action is excluded' do
|
||||
info = described_class.new({ index: false }, nil, nil, false)
|
||||
|
||||
expect(info.should_run?(dummy_controller)).to be_falsy
|
||||
end
|
||||
|
||||
it 'runs when the `if` conditional is true' do
|
||||
info = described_class.new({}, -> { true }, nil, false)
|
||||
|
||||
expect(info.should_run?(dummy_controller)).to be_truthy
|
||||
end
|
||||
|
||||
it 'does not run when the if condition is false' do
|
||||
info = described_class.new({}, -> { false }, nil, false)
|
||||
|
||||
expect(info.should_run?(dummy_controller)).to be_falsy
|
||||
end
|
||||
|
||||
it 'does not run when the `unless` check is true' do
|
||||
info = described_class.new({}, nil, -> { true }, false)
|
||||
|
||||
expect(info.should_run?(dummy_controller)).to be_falsy
|
||||
end
|
||||
|
||||
it 'runs when the `unless` check is false' do
|
||||
info = described_class.new({}, nil, -> { false }, false)
|
||||
|
||||
expect(info.should_run?(dummy_controller)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns the the oposite of #should_skip? when the check is a skip' do
|
||||
info = described_class.new({}, nil, nil, true)
|
||||
|
||||
expect(info).to receive(:should_skip?).with(dummy_controller).and_return(false)
|
||||
expect(info.should_run?(dummy_controller)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#should_skip?' do
|
||||
it 'skips when an action is defined' do
|
||||
info = described_class.new({ index: true }, nil, nil, true)
|
||||
|
||||
expect(info.should_skip?(dummy_controller)).to be_truthy
|
||||
end
|
||||
|
||||
it 'does not skip when the action is not defined' do
|
||||
info = described_class.new({}, nil, nil, true)
|
||||
|
||||
expect(info.should_skip?(dummy_controller)).to be_falsy
|
||||
end
|
||||
|
||||
it 'does not skip when the action is excluded' do
|
||||
info = described_class.new({ index: false }, nil, nil, true)
|
||||
|
||||
expect(info.should_skip?(dummy_controller)).to be_falsy
|
||||
end
|
||||
|
||||
it 'skips when the `if` conditional is true' do
|
||||
info = described_class.new({ index: true }, -> { true }, nil, true)
|
||||
|
||||
expect(info.should_skip?(dummy_controller)).to be_truthy
|
||||
end
|
||||
|
||||
it 'does not skip the `if` conditional is false' do
|
||||
info = described_class.new({ index: true }, -> { false }, nil, true)
|
||||
|
||||
expect(info.should_skip?(dummy_controller)).to be_falsy
|
||||
end
|
||||
|
||||
it 'does not skip when the `unless` check is true' do
|
||||
info = described_class.new({ index: true }, nil, -> { true }, true)
|
||||
|
||||
expect(info.should_skip?(dummy_controller)).to be_falsy
|
||||
end
|
||||
|
||||
it 'skips when `unless` check is false' do
|
||||
info = described_class.new({ index: true }, nil, -> { false }, true)
|
||||
|
||||
expect(info.should_skip?(dummy_controller)).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns the the oposite of #should_run? when the check is not a skip' do
|
||||
info = described_class.new({}, nil, nil, false)
|
||||
|
||||
expect(info).to receive(:should_run?).with(dummy_controller).and_return(false)
|
||||
expect(info.should_skip?(dummy_controller)).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue