Port `read_cross_project` ability from EE

This commit is contained in:
Bob Van Landuyt 2017-12-11 15:21:06 +01:00
parent b5306075c2
commit 148816cd67
115 changed files with 2530 additions and 346 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -1,6 +1,10 @@
class Dashboard::ApplicationController < ApplicationController
include ControllerWithCrossProjectAccessCheck
layout 'dashboard'
requires_cross_project_access
private
def projects

View File

@ -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)

View File

@ -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])

View File

@ -1,4 +1,6 @@
class Dashboard::SnippetsController < Dashboard::ApplicationController
skip_cross_project_access_check :index
def index
@snippets = SnippetsFinder.new(
current_user,

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)}"

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -1,4 +1,6 @@
class MergeRequestTargetProjectFinder
include FinderMethods
attr_reader :current_user, :source_project
def initialize(current_user: nil, source_project:)

View File

@ -8,6 +8,8 @@
# state - filters by state.
class MilestonesFinder
include FinderMethods
attr_reader :params, :project_ids, :group_ids
def initialize(params = {})

View File

@ -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)

View File

@ -13,6 +13,11 @@
#
class TodosFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
requires_cross_project_access unless: -> { project? }
NONE = '0'.freeze
attr_accessor :current_user, :params

View 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

View File

@ -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

View File

@ -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

View File

@ -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 =

View File

@ -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'

View File

@ -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

View File

@ -213,6 +213,7 @@ module ProjectsHelper
controller.controller_name,
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
'v2.5'
]
@ -608,4 +609,8 @@ module ProjectsHelper
project_find_file_path(@project, ref)
end
def can_show_last_commit_in_list?(project)
can?(current_user, :read_cross_project) && project.commit
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1036,6 +1036,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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -1,3 +1,3 @@
class MergeRequestPolicy < IssuablePolicy
# pass
rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"}

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Authorize project access with an external service
merge_request: 4675
author:
type: added

View 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

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,40 @@
module Banzai
module Filter
# HTML filter that removes sensitive information from cross project
# issue references.
#
# The link to the issue or merge request is preserved only the IID is shown,
# but all other info is removed.
class CrossProjectIssuableInformationFilter < HTML::Pipeline::Filter
def call
return doc if can_read_cross_project?
extractor = Banzai::IssuableExtractor.new(project, current_user)
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
next if issuable.project == project
node['class'] = node['class'].gsub('has-tooltip', '')
node['title'] = nil
end
doc
end
private
def project
context[:project]
end
def can_read_cross_project?
Ability.allowed?(current_user, :read_cross_project)
end
def current_user
context[:current_user]
end
end
end
end

View File

@ -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

View File

@ -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)

View File

@ -6,6 +6,7 @@ module Banzai
Filter::RedactorFilter,
Filter::RelativeLinkFilter,
Filter::IssuableStateFilter,
Filter::CrossProjectIssuableInformationFilter,
Filter::AbsoluteLinkFilter
]
end

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View 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

View 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

View File

@ -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

View File

@ -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|Youre about to retry pipeline %{id}."
msgstr ""
msgid "Pipeline|Youre 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 projects environments"
msgstr ""
msgid "PrometheusService|By default, Prometheus listens on http://localhost:9090. Its 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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View 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

View File

@ -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

View File

@ -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) }

View 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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,50 @@
require 'spec_helper'
describe Banzai::Filter::CrossProjectIssuableInformationFilter do
include ActionView::Helpers::UrlHelper
include FilterSpecHelper
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:context) { { project: project, current_user: user } }
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
context 'when the user cannot read cross project' do
before do
allow(Ability).to receive(:allowed?) { false }
end
it 'skips links to issues within the same project' do
issue = create(:issue, project: project)
link = create_link(issue)
doc = filter(link, context)
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 = filter(link, context)
result = doc.css('a').last
expect(result['class']).not_to include('has-tooltip')
expect(result['title']).to be_empty
end
end
end

View File

@ -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 }

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More