summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/application_controller.rb11
-rw-r--r--app/controllers/boards/issues_controller.rb2
-rw-r--r--app/controllers/concerns/controller_with_cross_project_access_check.rb24
-rw-r--r--app/controllers/concerns/routable_actions.rb8
-rw-r--r--app/controllers/dashboard/application_controller.rb4
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb1
-rw-r--r--app/controllers/dashboard/snippets_controller.rb2
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/avatars_controller.rb2
-rw-r--r--app/controllers/groups/children_controller.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb4
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb1
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb6
-rw-r--r--app/controllers/oauth/applications_controller.rb3
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb4
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb6
-rw-r--r--app/controllers/search_controller.rb9
-rw-r--r--app/controllers/users_controller.rb20
-rw-r--r--app/finders/concerns/finder_methods.rb51
-rw-r--r--app/finders/concerns/finder_with_cross_project_access.rb70
-rw-r--r--app/finders/events_finder.rb4
-rw-r--r--app/finders/issuable_finder.rb16
-rw-r--r--app/finders/labels_finder.rb4
-rw-r--r--app/finders/merge_request_target_project_finder.rb2
-rw-r--r--app/finders/milestones_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb10
-rw-r--r--app/finders/todos_finder.rb5
-rw-r--r--app/finders/user_recent_events_finder.rb33
-rw-r--r--app/helpers/dashboard_helper.rb24
-rw-r--r--app/helpers/explore_helper.rb16
-rw-r--r--app/helpers/groups_helper.rb22
-rw-r--r--app/helpers/issues_helper.rb21
-rw-r--r--app/helpers/nav_helper.rb32
-rw-r--r--app/helpers/projects_helper.rb5
-rw-r--r--app/helpers/users_helper.rb14
-rw-r--r--app/models/ability.rb30
-rw-r--r--app/models/concerns/protected_ref_access.rb3
-rw-r--r--app/models/issue.rb13
-rw-r--r--app/models/notification_recipient.rb1
-rw-r--r--app/models/project.rb3
-rw-r--r--app/policies/base_policy.rb3
-rw-r--r--app/policies/issuable_policy.rb13
-rw-r--r--app/policies/issue_policy.rb3
-rw-r--r--app/policies/merge_request_policy.rb2
-rw-r--r--app/policies/project_policy.rb28
-rw-r--r--app/serializers/group_child_entity.rb17
-rw-r--r--app/services/issuable_base_service.rb2
-rw-r--r--app/views/errors/access_denied.html.haml10
-rw-r--r--app/views/layouts/header/_default.html.haml27
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml89
-rw-r--r--app/views/layouts/nav/_explore.html.haml21
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml153
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/users/show.html.haml77
57 files changed, 698 insertions, 248 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b04bfaf3e49..e6a41202f04 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index ee23ee0bcc3..352f12a89fd 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -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
diff --git a/app/controllers/concerns/controller_with_cross_project_access_check.rb b/app/controllers/concerns/controller_with_cross_project_access_check.rb
new file mode 100644
index 00000000000..a45c3384578
--- /dev/null
+++ b/app/controllers/concerns/controller_with_cross_project_access_check.rb
@@ -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
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index f745deb083c..0931bdf4c04 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -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)
diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
index 9d3d1c23c28..9fb5c525425 100644
--- a/app/controllers/dashboard/application_controller.rb
+++ b/app/controllers/dashboard/application_controller.rb
@@ -1,6 +1,10 @@
class Dashboard::ApplicationController < ApplicationController
+ include ControllerWithCrossProjectAccessCheck
+
layout 'dashboard'
+ requires_cross_project_access
+
private
def projects
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 025769f512a..79f563bef86 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -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)
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index de9f8f9224a..4d4ac025f8c 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -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])
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index 8dd91264451..0ba97e4fd59 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,4 +1,6 @@
class Dashboard::SnippetsController < Dashboard::ApplicationController
+ skip_cross_project_access_check :index
+
def index
@snippets = SnippetsFinder.new(
current_user,
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 96ce686c989..4a2bfc1f887 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -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
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index 735915abdaa..cc5ba5878f8 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -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
diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb
index b474f5d15ee..0e8125d6113 100644
--- a/app/controllers/groups/children_controller.rb
+++ b/app/controllers/groups/children_controller.rb
@@ -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?
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 21e77431176..2c371e76313 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -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]
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 0142ad8278c..4bf6a2a3ad1 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -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
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 913e13bf734..cb8771bc97e 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -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
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 7d129c5dece..14b9d6c22bd 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -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
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 6a21a3f77ad..a1fe02dc852 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -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
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 45c66b63ea5..992c8ea6992 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -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
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 35e67730a27..74c25505e36 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -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)}"
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index a5a2d54ba82..a90030a8312 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -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
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index fbad9ba7db8..983f888b8ec 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -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
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 575ec5c20f0..956df4a0a16 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -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
diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb
new file mode 100644
index 00000000000..2e905fa5750
--- /dev/null
+++ b/app/finders/concerns/finder_methods.rb
@@ -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
diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb
new file mode 100644
index 00000000000..92bf98d7cd2
--- /dev/null
+++ b/app/finders/concerns/finder_with_cross_project_access.rb
@@ -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
diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb
index 46ecbaba73a..8676925a540 100644
--- a/app/finders/events_finder.rb
+++ b/app/finders/events_finder.rb
@@ -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:
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 384a336e2bb..9dd6634b38f 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -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)
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 1427cdaa382..f013e177c5b 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -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
diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb
index 189eb3847eb..f358938344e 100644
--- a/app/finders/merge_request_target_project_finder.rb
+++ b/app/finders/merge_request_target_project_finder.rb
@@ -1,4 +1,6 @@
class MergeRequestTargetProjectFinder
+ include FinderMethods
+
attr_reader :current_user, :source_project
def initialize(current_user: nil, source_project:)
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index b4605fca193..f5d2b9f253a 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -8,6 +8,8 @@
# state - filters by state.
class MilestonesFinder
+ include FinderMethods
+
attr_reader :params, :project_ids, :group_ids
def initialize(params = {})
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index ec61fe1892e..a73c573736e 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -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)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 3502bf08971..edb17843002 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -13,6 +13,11 @@
#
class TodosFinder
+ prepend FinderWithCrossProjectAccess
+ include FinderMethods
+
+ requires_cross_project_access unless: -> { project? }
+
NONE = '0'.freeze
attr_accessor :current_user, :params
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
new file mode 100644
index 00000000000..6f7f7c30d92
--- /dev/null
+++ b/app/finders/user_recent_events_finder.rb
@@ -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
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index c25b54eadc6..19aa55a8d49 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -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
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index b981a1e8242..f062a91a166 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -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
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 23de3590b93..5fbaa17c40e 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -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 =
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 64cd3032780..0f25d401406 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -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'
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 680ea96a556..56c88e6eab0 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -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
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index b97b72d62c3..db1c67262d7 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -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
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index b5f54d3e154..01af68088df 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -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
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 0b6bcbde5d9..6dae49f38dc 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -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
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 80c9f7d4eb4..bfda5b1678b 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -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
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 93628b456f2..c81f7e52bb1 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -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
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 472b348a545..fd70e920c7e 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -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
diff --git a/app/models/project.rb b/app/models/project.rb
index 79058d51af8..e2a44455b89 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 8fa7b2753c7..603218aa6df 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -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
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index f0aa16d2ecf..3f6d7d04667 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -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"
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index bd2d417b2a8..ed499511999 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -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
diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb
index bc3afc626fb..e003376d219 100644
--- a/app/policies/merge_request_policy.rb
+++ b/app/policies/merge_request_policy.rb
@@ -1,3 +1,3 @@
class MergeRequestPolicy < IssuablePolicy
- # pass
+ rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 61a7bf02675..3b0550b4dd6 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -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
diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb
index aca4e4ca488..15ec0f89bb2 100644
--- a/app/serializers/group_child_entity.rb
+++ b/app/serializers/group_child_entity.rb
@@ -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
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e7463e6e25c..66a9b1f82e0 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -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
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index a97cbd4d4b3..bf540439c79 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -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"}
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 1d00ae928f6..e6238c0dddb 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -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'
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 74532eba298..f773bd0832d 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -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
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index cd1c39f3226..50bde9d1754 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -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'
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 09a43a2cac5..47ae79b7a69 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -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
+
+ - 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: 'labels#index') do
- = link_to group_labels_path(@group), title: 'Labels' do
- %span
- Labels
+ - if group_sidebar_link?(:milestones)
+ = nav_link(path: 'milestones#index') do
+ = link_to group_milestones_path(@group), title: 'Milestones' do
+ %span
+ Milestones
+
+ - 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)
- = nav_link(path: 'milestones#index') do
- = link_to group_milestones_path(@group), title: 'Milestones' do
- %span
- Milestones
+ - 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') }
- = 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?(:settings)
= nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
.nav-icon-container
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 33435216c14..0687f6d961d 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -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
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index a396d1007a7..4bf01ecb48c 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -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