From 148816cd67a314f17e79c107270cc708501bdd39 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 11 Dec 2017 15:21:06 +0100 Subject: Port `read_cross_project` ability from EE --- app/controllers/application_controller.rb | 11 +- app/controllers/boards/issues_controller.rb | 2 +- .../controller_with_cross_project_access_check.rb | 24 ++++ app/controllers/concerns/routable_actions.rb | 8 +- .../dashboard/application_controller.rb | 4 + app/controllers/dashboard/groups_controller.rb | 2 + app/controllers/dashboard/projects_controller.rb | 1 + app/controllers/dashboard/snippets_controller.rb | 2 + app/controllers/groups/application_controller.rb | 2 + app/controllers/groups/avatars_controller.rb | 2 + app/controllers/groups/children_controller.rb | 1 + app/controllers/groups/group_members_controller.rb | 4 + .../groups/settings/ci_cd_controller.rb | 1 + app/controllers/groups/variables_controller.rb | 2 + app/controllers/groups_controller.rb | 6 + app/controllers/oauth/applications_controller.rb | 3 + .../projects/autocomplete_sources_controller.rb | 4 +- app/controllers/projects/blob_controller.rb | 2 +- .../merge_requests/creations_controller.rb | 6 +- app/controllers/search_controller.rb | 9 +- app/controllers/users_controller.rb | 20 +-- app/finders/concerns/finder_methods.rb | 51 +++++++ .../concerns/finder_with_cross_project_access.rb | 70 ++++++++++ app/finders/events_finder.rb | 4 + app/finders/issuable_finder.rb | 16 +-- app/finders/labels_finder.rb | 4 + app/finders/merge_request_target_project_finder.rb | 2 + app/finders/milestones_finder.rb | 2 + app/finders/snippets_finder.rb | 10 +- app/finders/todos_finder.rb | 5 + app/finders/user_recent_events_finder.rb | 33 +++++ app/helpers/dashboard_helper.rb | 24 ++++ app/helpers/explore_helper.rb | 16 +++ app/helpers/groups_helper.rb | 22 +++ app/helpers/issues_helper.rb | 21 --- app/helpers/nav_helper.rb | 32 +++++ app/helpers/projects_helper.rb | 5 + app/helpers/users_helper.rb | 14 ++ app/models/ability.rb | 30 +++- app/models/concerns/protected_ref_access.rb | 3 +- app/models/issue.rb | 13 +- app/models/notification_recipient.rb | 1 + app/models/project.rb | 3 + app/policies/base_policy.rb | 3 + app/policies/issuable_policy.rb | 13 ++ app/policies/issue_policy.rb | 3 + app/policies/merge_request_policy.rb | 2 +- app/policies/project_policy.rb | 28 ++-- app/serializers/group_child_entity.rb | 17 ++- app/services/issuable_base_service.rb | 2 +- app/views/errors/access_denied.html.haml | 10 +- app/views/layouts/header/_default.html.haml | 27 ++-- app/views/layouts/nav/_dashboard.html.haml | 89 ++++++------ app/views/layouts/nav/_explore.html.haml | 21 +-- app/views/layouts/nav/sidebar/_group.html.haml | 153 +++++++++++---------- app/views/shared/projects/_project.html.haml | 4 +- app/views/users/show.html.haml | 77 ++++++----- .../bvl-external-policy-classification.yml | 5 + config/initializers/0_as_concern.rb | 25 ++++ lib/api/helpers.rb | 2 +- lib/api/settings.rb | 2 +- .../cross_project_issuable_information_filter.rb | 40 ++++++ lib/banzai/filter/issuable_state_filter.rb | 6 + lib/banzai/filter/milestone_reference_filter.rb | 2 +- lib/banzai/pipeline/post_process_pipeline.rb | 1 + lib/banzai/reference_parser/issuable_parser.rb | 2 +- lib/banzai/reference_parser/issue_parser.rb | 25 +++- lib/gitlab/contributions_calendar.rb | 6 + lib/gitlab/cross_project_access.rb | 67 +++++++++ .../cross_project_access/check_collection.rb | 47 +++++++ lib/gitlab/cross_project_access/check_info.rb | 66 +++++++++ lib/gitlab/cross_project_access/class_methods.rb | 48 +++++++ lib/gitlab/user_access.rb | 2 +- locale/gitlab.pot | 133 +++++++++++++++++- spec/controllers/boards/issues_controller_spec.rb | 1 + ...troller_with_cross_project_access_check_spec.rb | 146 ++++++++++++++++++++ .../merge_requests/creations_controller_spec.rb | 64 ++++++++- spec/controllers/search_controller_spec.rb | 26 ++++ spec/controllers/users_controller_spec.rb | 25 ++++ spec/features/users/show_spec.rb | 17 +++ spec/finders/concerns/finder_methods_spec.rb | 70 ++++++++++ .../finder_with_cross_project_access_spec.rb | 118 ++++++++++++++++ spec/finders/events_finder_spec.rb | 8 ++ spec/finders/milestones_finder_spec.rb | 8 ++ spec/finders/snippets_finder_spec.rb | 24 +++- spec/finders/user_recent_events_finder_spec.rb | 31 +++++ spec/helpers/dashboard_helper_spec.rb | 24 ++++ spec/helpers/explore_helper_spec.rb | 18 +++ spec/helpers/groups_helper_spec.rb | 35 +++++ spec/helpers/issues_helper_spec.rb | 15 -- spec/helpers/nav_helper_spec.rb | 53 +++++++ spec/helpers/projects_helper_spec.rb | 10 ++ spec/helpers/users_helper_spec.rb | 13 ++ spec/lib/banzai/commit_renderer_spec.rb | 2 +- ...oss_project_issuable_information_filter_spec.rb | 50 +++++++ .../banzai/filter/issuable_state_filter_spec.rb | 8 ++ spec/lib/banzai/redactor_spec.rb | 2 +- .../banzai/reference_parser/issue_parser_spec.rb | 47 ++++++- spec/lib/gitlab/contributions_calendar_spec.rb | 13 ++ .../cross_project_access/check_collection_spec.rb | 55 ++++++++ .../gitlab/cross_project_access/check_info_spec.rb | 111 +++++++++++++++ .../cross_project_access/class_methods_spec.rb | 46 +++++++ spec/lib/gitlab/cross_project_access_spec.rb | 84 +++++++++++ spec/models/ability_spec.rb | 95 +++++++++++++ spec/models/concerns/protected_ref_access_spec.rb | 31 +++++ spec/models/issue_spec.rb | 50 +++++-- spec/models/notification_recipient_spec.rb | 16 +++ spec/models/project_spec.rb | 7 + spec/policies/issuable_policy_spec.rb | 8 +- spec/policies/issue_policy_spec.rb | 88 ++++++------ .../create_from_issue_service_spec.rb | 2 +- spec/services/todo_service_spec.rb | 3 +- spec/spec_helper.rb | 8 ++ spec/support/snippet_visibility.rb | 18 +++ .../shared/projects/_project.html.haml_spec.rb | 1 + 115 files changed, 2530 insertions(+), 346 deletions(-) create mode 100644 app/controllers/concerns/controller_with_cross_project_access_check.rb create mode 100644 app/finders/concerns/finder_methods.rb create mode 100644 app/finders/concerns/finder_with_cross_project_access.rb create mode 100644 app/finders/user_recent_events_finder.rb create mode 100644 changelogs/unreleased-ee/bvl-external-policy-classification.yml create mode 100644 config/initializers/0_as_concern.rb create mode 100644 lib/banzai/filter/cross_project_issuable_information_filter.rb create mode 100644 lib/gitlab/cross_project_access.rb create mode 100644 lib/gitlab/cross_project_access/check_collection.rb create mode 100644 lib/gitlab/cross_project_access/check_info.rb create mode 100644 lib/gitlab/cross_project_access/class_methods.rb create mode 100644 spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb create mode 100644 spec/features/users/show_spec.rb create mode 100644 spec/finders/concerns/finder_methods_spec.rb create mode 100644 spec/finders/concerns/finder_with_cross_project_access_spec.rb create mode 100644 spec/finders/user_recent_events_finder_spec.rb create mode 100644 spec/helpers/dashboard_helper_spec.rb create mode 100644 spec/helpers/explore_helper_spec.rb create mode 100644 spec/helpers/nav_helper_spec.rb create mode 100644 spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb create mode 100644 spec/lib/gitlab/cross_project_access/check_collection_spec.rb create mode 100644 spec/lib/gitlab/cross_project_access/check_info_spec.rb create mode 100644 spec/lib/gitlab/cross_project_access/class_methods_spec.rb create mode 100644 spec/lib/gitlab/cross_project_access_spec.rb create mode 100644 spec/models/concerns/protected_ref_access_spec.rb create mode 100644 spec/models/notification_recipient_spec.rb 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 diff --git a/changelogs/unreleased-ee/bvl-external-policy-classification.yml b/changelogs/unreleased-ee/bvl-external-policy-classification.yml new file mode 100644 index 00000000000..074629c8c12 --- /dev/null +++ b/changelogs/unreleased-ee/bvl-external-policy-classification.yml @@ -0,0 +1,5 @@ +--- +title: Authorize project access with an external service +merge_request: 4675 +author: +type: added diff --git a/config/initializers/0_as_concern.rb b/config/initializers/0_as_concern.rb new file mode 100644 index 00000000000..40232bd6252 --- /dev/null +++ b/config/initializers/0_as_concern.rb @@ -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 diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 6134ad2bfc7..e4fca77ab5d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -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) diff --git a/lib/api/settings.rb b/lib/api/settings.rb index cee4d309816..152df23a327 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -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) diff --git a/lib/banzai/filter/cross_project_issuable_information_filter.rb b/lib/banzai/filter/cross_project_issuable_information_filter.rb new file mode 100644 index 00000000000..c2c08b4fd6a --- /dev/null +++ b/lib/banzai/filter/cross_project_issuable_information_filter.rb @@ -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 diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb index 327ea9449a1..77299abe324 100644 --- a/lib/banzai/filter/issuable_state_filter.rb +++ b/lib/banzai/filter/issuable_state_filter.rb @@ -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 diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 2a6b0964ac5..8ec696ce5fc 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -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) diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb index dcd52bc03c7..4cf97e2d9d2 100644 --- a/lib/banzai/pipeline/post_process_pipeline.rb +++ b/lib/banzai/pipeline/post_process_pipeline.rb @@ -6,6 +6,7 @@ module Banzai Filter::RedactorFilter, Filter::RelativeLinkFilter, Filter::IssuableStateFilter, + Filter::CrossProjectIssuableInformationFilter, Filter::AbsoluteLinkFilter ] end diff --git a/lib/banzai/reference_parser/issuable_parser.rb b/lib/banzai/reference_parser/issuable_parser.rb index 3953867eb83..fad127d7e5b 100644 --- a/lib/banzai/reference_parser/issuable_parser.rb +++ b/lib/banzai/reference_parser/issuable_parser.rb @@ -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 diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index 38d4e3f3e44..230827129b6 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -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 diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 0735243e021..9576d5a3fd8 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -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 diff --git a/lib/gitlab/cross_project_access.rb b/lib/gitlab/cross_project_access.rb new file mode 100644 index 00000000000..6eaed51b64c --- /dev/null +++ b/lib/gitlab/cross_project_access.rb @@ -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 diff --git a/lib/gitlab/cross_project_access/check_collection.rb b/lib/gitlab/cross_project_access/check_collection.rb new file mode 100644 index 00000000000..88376232065 --- /dev/null +++ b/lib/gitlab/cross_project_access/check_collection.rb @@ -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 diff --git a/lib/gitlab/cross_project_access/check_info.rb b/lib/gitlab/cross_project_access/check_info.rb new file mode 100644 index 00000000000..e8a845c7f1e --- /dev/null +++ b/lib/gitlab/cross_project_access/check_info.rb @@ -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 diff --git a/lib/gitlab/cross_project_access/class_methods.rb b/lib/gitlab/cross_project_access/class_methods.rb new file mode 100644 index 00000000000..90eac94800c --- /dev/null +++ b/lib/gitlab/cross_project_access/class_methods.rb @@ -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 diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 15eb1c41213..ff4dc29efea 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fadc17a659d..889a03e7859 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -150,6 +150,39 @@ msgstr "" msgid "AdminHealthPageLink|health page" msgstr "" +msgid "AdminProjects|Delete" +msgstr "" + +msgid "AdminProjects|Delete Project %{projectName}?" +msgstr "" + +msgid "AdminProjects|Delete project" +msgstr "" + +msgid "AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages." +msgstr "" + +msgid "AdminUsers|Block user" +msgstr "" + +msgid "AdminUsers|Delete User %{username} and contributions?" +msgstr "" + +msgid "AdminUsers|Delete User %{username}?" +msgstr "" + +msgid "AdminUsers|Delete user" +msgstr "" + +msgid "AdminUsers|Delete user and contributions" +msgstr "" + +msgid "AdminUsers|To confirm, type %{projectName}" +msgstr "" + +msgid "AdminUsers|To confirm, type %{username}" +msgstr "" + msgid "Advanced settings" msgstr "" @@ -177,9 +210,21 @@ msgstr "" msgid "An error occurred while getting projects" msgstr "" +msgid "An error occurred while importing project" +msgstr "" + +msgid "An error occurred while loading commits" +msgstr "" + +msgid "An error occurred while loading diff" +msgstr "" + msgid "An error occurred while loading filenames" msgstr "" +msgid "An error occurred while loading the file" +msgstr "" + msgid "An error occurred while rendering KaTeX" msgstr "" @@ -192,6 +237,9 @@ msgstr "" msgid "An error occurred while retrieving diff" msgstr "" +msgid "An error occurred while saving assignees" +msgstr "" + msgid "An error occurred while validating username" msgstr "" @@ -1018,6 +1066,9 @@ msgstr "" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "" +msgid "Create branch" +msgstr "" + msgid "Create directory" msgstr "" @@ -1033,6 +1084,9 @@ msgstr "" msgid "Create merge request" msgstr "" +msgid "Create merge request and branch" +msgstr "" + msgid "Create new branch" msgstr "" @@ -1290,9 +1344,15 @@ msgstr "" msgid "Failed to change the owner" msgstr "" +msgid "Failed to remove issue from board, please try again." +msgstr "" + msgid "Failed to remove the pipeline schedule" msgstr "" +msgid "Failed to update issues, please try again." +msgstr "" + msgid "Feb" msgstr "" @@ -1985,6 +2045,24 @@ msgstr "" msgid "Pipelines|Get started with Pipelines" msgstr "" +msgid "Pipeline|Retry pipeline" +msgstr "" + +msgid "Pipeline|Retry pipeline #%{id}?" +msgstr "" + +msgid "Pipeline|Stop pipeline" +msgstr "" + +msgid "Pipeline|Stop pipeline #%{id}?" +msgstr "" + +msgid "Pipeline|You’re about to retry pipeline %{id}." +msgstr "" + +msgid "Pipeline|You’re about to stop pipeline %{id}." +msgstr "" + msgid "Pipeline|all" msgstr "" @@ -2144,12 +2222,30 @@ msgstr "" msgid "ProjectsDropdown|This feature requires browser localStorage support" msgstr "" +msgid "PrometheusService|Active" +msgstr "" + +msgid "PrometheusService|Auto configuration" +msgstr "" + +msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments" +msgstr "" + msgid "PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server." msgstr "" msgid "PrometheusService|Finding and configuring metrics..." msgstr "" +msgid "PrometheusService|Install Prometheus on clusters" +msgstr "" + +msgid "PrometheusService|Manage clusters" +msgstr "" + +msgid "PrometheusService|Manual configuration" +msgstr "" + msgid "PrometheusService|Metrics" msgstr "" @@ -2171,9 +2267,18 @@ msgstr "" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgstr "" +msgid "PrometheusService|Prometheus is being automatically managed on your clusters" +msgstr "" + msgid "PrometheusService|Time-series monitoring service" msgstr "" +msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters" +msgstr "" + +msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below" +msgstr "" + msgid "PrometheusService|View environments" msgstr "" @@ -2376,12 +2481,18 @@ msgstr "" msgid "Something went wrong when toggling the button" msgstr "" +msgid "Something went wrong while closing the issue. Please try again later" +msgstr "" + msgid "Something went wrong while fetching the projects." msgstr "" msgid "Something went wrong while fetching the registry list." msgstr "" +msgid "Something went wrong while reopening the issue. Please try again later" +msgstr "" + msgid "Something went wrong. Please try again." msgstr "" @@ -2478,6 +2589,9 @@ msgstr "" msgid "Source" msgstr "" +msgid "Source (branch or tag)" +msgstr "" + msgid "Source code" msgstr "" @@ -2738,6 +2852,9 @@ msgstr "" msgid "This merge request is locked." msgstr "" +msgid "This page is unavailable because you are not allowed to read information across multiple projects." +msgstr "" + msgid "This project" msgstr "" @@ -2934,9 +3051,6 @@ msgstr "" msgid "Trigger this manual action" msgstr "" -msgid "Type %{value} to confirm:" -msgstr "" - msgid "Unable to reset project cache." msgstr "" @@ -3229,6 +3343,9 @@ msgid_plural "merge requests" msgstr[0] "" msgstr[1] "" +msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" +msgstr "" + msgid "mrWidget|Cancel automatic merge" msgstr "" @@ -3262,6 +3379,9 @@ msgstr "" msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the" msgstr "" +msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line" +msgstr "" + msgid "mrWidget|Mentions" msgstr "" @@ -3349,6 +3469,9 @@ msgstr "" msgid "mrWidget|You can remove source branch now" msgstr "" +msgid "mrWidget|branch does not exist." +msgstr "" + msgid "mrWidget|command line" msgstr "" diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 79bbc29e80d..4770e187db6 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -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 diff --git a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb new file mode 100644 index 00000000000..27f558e1b5d --- /dev/null +++ b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb @@ -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 diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb index 92db7284e0e..24310b847e8 100644 --- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb @@ -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 diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 37f961d0c94..30c06ddf744 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -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 diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 2898c4b119e..b0acf4a49ac 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -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 diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb new file mode 100644 index 00000000000..b5bbb2c0ea5 --- /dev/null +++ b/spec/features/users/show_spec.rb @@ -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 diff --git a/spec/finders/concerns/finder_methods_spec.rb b/spec/finders/concerns/finder_methods_spec.rb new file mode 100644 index 00000000000..a4ad331f613 --- /dev/null +++ b/spec/finders/concerns/finder_methods_spec.rb @@ -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 diff --git a/spec/finders/concerns/finder_with_cross_project_access_spec.rb b/spec/finders/concerns/finder_with_cross_project_access_spec.rb new file mode 100644 index 00000000000..c784fb87972 --- /dev/null +++ b/spec/finders/concerns/finder_with_cross_project_access_spec.rb @@ -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 diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb index 18d6c0cfd74..62968e83292 100644 --- a/spec/finders/events_finder_spec.rb +++ b/spec/finders/events_finder_spec.rb @@ -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 diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index 0b3cf7ece5f..656d120311a 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -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 diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 54a07eccaba..1ae0bd988f2 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -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 diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb new file mode 100644 index 00000000000..3ca0f7c3c89 --- /dev/null +++ b/spec/finders/user_recent_events_finder_spec.rb @@ -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 diff --git a/spec/helpers/dashboard_helper_spec.rb b/spec/helpers/dashboard_helper_spec.rb new file mode 100644 index 00000000000..7ba24ba2956 --- /dev/null +++ b/spec/helpers/dashboard_helper_spec.rb @@ -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 diff --git a/spec/helpers/explore_helper_spec.rb b/spec/helpers/explore_helper_spec.rb new file mode 100644 index 00000000000..12651d80e36 --- /dev/null +++ b/spec/helpers/explore_helper_spec.rb @@ -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 diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 5f608fe18d9..b48c252acd3 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -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 diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index ddf881a7b6f..aeef5352333 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -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) } diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb new file mode 100644 index 00000000000..e840c927d59 --- /dev/null +++ b/spec/helpers/nav_helper_spec.rb @@ -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 diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index b67fee2fcc0..ad54f75cd17 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -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) diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index 03f78de8e91..6332217b920 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -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 diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb index 84adaebdcbe..e7ebb2a332f 100644 --- a/spec/lib/banzai/commit_renderer_spec.rb +++ b/spec/lib/banzai/commit_renderer_spec.rb @@ -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 diff --git a/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb b/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb new file mode 100644 index 00000000000..92b8c526e88 --- /dev/null +++ b/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb @@ -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 diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb index cacb33d3372..17347768a49 100644 --- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb +++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb @@ -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 } diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 2424c3fdc66..893a10d1b90 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -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) } diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 4cef3bdb24b..0a63567ee40 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -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 diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index f1655854486..49a179ba875 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -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 diff --git a/spec/lib/gitlab/cross_project_access/check_collection_spec.rb b/spec/lib/gitlab/cross_project_access/check_collection_spec.rb new file mode 100644 index 00000000000..a9e7575240e --- /dev/null +++ b/spec/lib/gitlab/cross_project_access/check_collection_spec.rb @@ -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 diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb new file mode 100644 index 00000000000..bc9dbf2bece --- /dev/null +++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' + +describe Gitlab::CrossProjectAccess::CheckInfo do + let(:dummy_controller) { double } + + before do + allow(dummy_controller).to receive(:action_name).and_return('index') + end + + describe '#should_run?' do + it 'runs when an action is defined' do + info = described_class.new({ index: true }, nil, nil, false) + + expect(info.should_run?(dummy_controller)).to be_truthy + end + + it 'runs when the action is missing' do + info = described_class.new({}, nil, nil, false) + + expect(info.should_run?(dummy_controller)).to be_truthy + end + + it 'does not run when the action is excluded' do + info = described_class.new({ index: false }, nil, nil, false) + + expect(info.should_run?(dummy_controller)).to be_falsy + end + + it 'runs when the `if` conditional is true' do + info = described_class.new({}, -> { true }, nil, false) + + expect(info.should_run?(dummy_controller)).to be_truthy + end + + it 'does not run when the if condition is false' do + info = described_class.new({}, -> { false }, nil, false) + + expect(info.should_run?(dummy_controller)).to be_falsy + end + + it 'does not run when the `unless` check is true' do + info = described_class.new({}, nil, -> { true }, false) + + expect(info.should_run?(dummy_controller)).to be_falsy + end + + it 'runs when the `unless` check is false' do + info = described_class.new({}, nil, -> { false }, false) + + expect(info.should_run?(dummy_controller)).to be_truthy + end + + it 'returns the the oposite of #should_skip? when the check is a skip' do + info = described_class.new({}, nil, nil, true) + + expect(info).to receive(:should_skip?).with(dummy_controller).and_return(false) + expect(info.should_run?(dummy_controller)).to be_truthy + end + end + + describe '#should_skip?' do + it 'skips when an action is defined' do + info = described_class.new({ index: true }, nil, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_truthy + end + + it 'does not skip when the action is not defined' do + info = described_class.new({}, nil, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_falsy + end + + it 'does not skip when the action is excluded' do + info = described_class.new({ index: false }, nil, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_falsy + end + + it 'skips when the `if` conditional is true' do + info = described_class.new({ index: true }, -> { true }, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_truthy + end + + it 'does not skip the `if` conditional is false' do + info = described_class.new({ index: true }, -> { false }, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_falsy + end + + it 'does not skip when the `unless` check is true' do + info = described_class.new({ index: true }, nil, -> { true }, true) + + expect(info.should_skip?(dummy_controller)).to be_falsy + end + + it 'skips when `unless` check is false' do + info = described_class.new({ index: true }, nil, -> { false }, true) + + expect(info.should_skip?(dummy_controller)).to be_truthy + end + + it 'returns the the oposite of #should_run? when the check is not a skip' do + info = described_class.new({}, nil, nil, false) + + expect(info).to receive(:should_run?).with(dummy_controller).and_return(false) + expect(info.should_skip?(dummy_controller)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/cross_project_access/class_methods_spec.rb b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb new file mode 100644 index 00000000000..5349685e633 --- /dev/null +++ b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::CrossProjectAccess::ClassMethods do + let(:dummy_class) do + Class.new do + extend Gitlab::CrossProjectAccess::ClassMethods + end + end + let(:dummy_proc) { lambda { false } } + + describe '#requires_cross_project_access' do + it 'creates a correct check when a hash is passed' do + expect(Gitlab::CrossProjectAccess) + .to receive(:add_check).with(dummy_class, + actions: { hello: true, world: false }, + positive_condition: dummy_proc, + negative_condition: dummy_proc) + + dummy_class.requires_cross_project_access( + hello: true, world: false, if: dummy_proc, unless: dummy_proc + ) + end + + it 'creates a correct check when an array is passed' do + expect(Gitlab::CrossProjectAccess) + .to receive(:add_check).with(dummy_class, + actions: { hello: true, world: true }, + positive_condition: nil, + negative_condition: nil) + + dummy_class.requires_cross_project_access(:hello, :world) + end + + it 'creates a correct check when an array and a hash is passed' do + expect(Gitlab::CrossProjectAccess) + .to receive(:add_check).with(dummy_class, + actions: { hello: true, world: true }, + positive_condition: dummy_proc, + negative_condition: dummy_proc) + + dummy_class.requires_cross_project_access( + :hello, :world, if: dummy_proc, unless: dummy_proc + ) + end + end +end diff --git a/spec/lib/gitlab/cross_project_access_spec.rb b/spec/lib/gitlab/cross_project_access_spec.rb new file mode 100644 index 00000000000..614b0473c7e --- /dev/null +++ b/spec/lib/gitlab/cross_project_access_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Gitlab::CrossProjectAccess do + let(:super_class) { Class.new } + let(:descendant_class) { Class.new(super_class) } + let(:current_instance) { described_class.new } + + before do + allow(described_class).to receive(:instance).and_return(current_instance) + end + + describe '#add_check' do + it 'keeps track of the properties to check' do + expect do + described_class.add_check(super_class, + actions: { index: true }, + positive_condition: -> { true }, + negative_condition: -> { false }) + end.to change { described_class.checks.size }.by(1) + end + + it 'builds the check correctly' do + check_collection = described_class.add_check(super_class, + actions: { index: true }, + positive_condition: -> { 'positive' }, + negative_condition: -> { 'negative' }) + + check = check_collection.checks.first + + expect(check.actions).to eq(index: true) + expect(check.positive_condition.call).to eq('positive') + expect(check.negative_condition.call).to eq('negative') + end + + it 'merges the checks of a parent class into existing checks of a subclass' do + subclass_collection = described_class.add_check(descendant_class) + + expect(subclass_collection).to receive(:add_collection).and_call_original + + described_class.add_check(super_class) + end + + it 'merges the existing checks of a superclass into the checks of a subclass' do + super_collection = described_class.add_check(super_class) + descendant_collection = described_class.add_check(descendant_class) + + expect(descendant_collection.checks).to include(*super_collection.checks) + end + end + + describe '#find_check' do + it 'returns a check when it was defined for a superclass' do + expected_check = described_class.add_check(super_class, + actions: { index: true }, + positive_condition: -> { 'positive' }, + negative_condition: -> { 'negative' }) + + expect(described_class.find_check(descendant_class.new)) + .to eq(expected_check) + end + + it 'caches the result for a subclass' do + described_class.add_check(super_class, + actions: { index: true }, + positive_condition: -> { 'positive' }, + negative_condition: -> { 'negative' }) + + expect(described_class.instance).to receive(:closest_parent).once.and_call_original + + 2.times { described_class.find_check(descendant_class.new) } + end + + it 'returns the checks for the closest class if there are more checks available' do + described_class.add_check(super_class, + actions: { index: true }) + expected_check = described_class.add_check(descendant_class, + actions: { index: true, show: false }) + + check = described_class.find_check(descendant_class.new) + + expect(check).to eq(expected_check) + end + end +end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 38fb98d4f50..cd175dba6da 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -204,6 +204,78 @@ describe Ability do end end + describe '.merge_requests_readable_by_user' do + context 'with an admin' do + it 'returns all merge requests' do + user = build(:user, admin: true) + merge_request = build(:merge_request) + + expect(described_class.merge_requests_readable_by_user([merge_request], user)) + .to eq([merge_request]) + end + end + + context 'without a user' do + it 'returns merge_requests that are publicly visible' do + hidden_merge_request = build(:merge_request) + visible_merge_request = build(:merge_request, source_project: build(:project, :public)) + + merge_requests = described_class + .merge_requests_readable_by_user([hidden_merge_request, visible_merge_request]) + + expect(merge_requests).to eq([visible_merge_request]) + end + end + + context 'with a user' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:cross_project_merge_request) do + create(:merge_request, source_project: create(:project, :public)) + end + let(:other_merge_request) { create(:merge_request) } + let(:all_merge_requests) do + [merge_request, cross_project_merge_request, other_merge_request] + end + + subject(:readable_merge_requests) do + described_class.merge_requests_readable_by_user(all_merge_requests, user) + end + + before do + project.add_developer(user) + end + + it 'returns projects visible to the user' do + expect(readable_merge_requests).to contain_exactly(merge_request, cross_project_merge_request) + end + + context 'when a user cannot read cross project and a filter is passed' do + before do + allow(described_class).to receive(:allowed?).and_call_original + expect(described_class).to receive(:allowed?).with(user, :read_cross_project) { false } + end + + subject(:readable_merge_requests) do + read_cross_project_filter = -> (merge_requests) do + merge_requests.select { |mr| mr.source_project == project } + end + described_class.merge_requests_readable_by_user( + all_merge_requests, user, + filters: { read_cross_project: read_cross_project_filter } + ) + end + + it 'returns only MRs of the specified project without checking access on others' do + expect(described_class).not_to receive(:allowed?).with(user, :read_merge_request, cross_project_merge_request) + + expect(readable_merge_requests).to contain_exactly(merge_request) + end + end + end + end + describe '.issues_readable_by_user' do context 'with an admin user' do it 'returns all given issues' do @@ -250,6 +322,29 @@ describe Ability do expect(issues).to eq([visible_issue]) end end + + context 'when the user cannot read cross project' do + let(:user) { create(:user) } + let(:issue) { create(:issue) } + let(:other_project_issue) { create(:issue) } + let(:project) { issue.project } + + before do + project.add_developer(user) + + allow(described_class).to receive(:allowed?).and_call_original + allow(described_class).to receive(:allowed?).with(user, :read_cross_project, any_args) { false } + end + + it 'excludes issues from other projects whithout checking separatly when passing a scope' do + expect(described_class).not_to receive(:allowed?).with(user, :read_issue, other_project_issue) + + filters = { read_cross_project: -> (issues) { issues.where(project: project) } } + result = described_class.issues_readable_by_user(Issue.all, user, filters: filters) + + expect(result).to contain_exactly(issue) + end + end end describe '.project_disabled_features_rules' do diff --git a/spec/models/concerns/protected_ref_access_spec.rb b/spec/models/concerns/protected_ref_access_spec.rb new file mode 100644 index 00000000000..a62ca391e25 --- /dev/null +++ b/spec/models/concerns/protected_ref_access_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe ProtectedRefAccess do + subject(:protected_ref_access) do + create(:protected_branch, :masters_can_push).push_access_levels.first + end + + let(:project) { protected_ref_access.project } + + describe '#check_access' do + it 'is always true for admins' do + admin = create(:admin) + + expect(protected_ref_access.check_access(admin)).to be_truthy + end + + it 'is true for masters' do + master = create(:user) + project.add_master(master) + + expect(protected_ref_access.check_access(master)).to be_truthy + end + + it 'is for developers of the project' do + developer = create(:user) + project.add_developer(developer) + + expect(protected_ref_access.check_access(developer)).to be_falsy + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index f5c9f551e65..feed7968f09 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -221,27 +221,55 @@ describe Issue do end describe '#referenced_merge_requests' do - it 'returns the referenced merge requests' do - project = create(:project, :public) - - mr1 = create(:merge_request, - source_project: project, - source_branch: 'master', - target_branch: 'feature') + let(:project) { create(:project, :public) } + let(:issue) do + create(:issue, description: merge_request.to_reference, project: project) + end + let!(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'master', + target_branch: 'feature') + end + it 'returns the referenced merge requests' do mr2 = create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') - issue = create(:issue, description: mr1.to_reference, project: project) - create(:note_on_issue, noteable: issue, note: mr2.to_reference, project_id: project.id) - expect(issue.referenced_merge_requests).to eq([mr1, mr2]) + expect(issue.referenced_merge_requests).to eq([merge_request, mr2]) + end + + it 'returns cross project referenced merge requests' do + other_project = create(:project, :public) + cross_project_merge_request = create(:merge_request, source_project: other_project) + create(:note_on_issue, + noteable: issue, + note: cross_project_merge_request.to_reference(issue.project), + project_id: issue.project.id) + + expect(issue.referenced_merge_requests).to eq([merge_request, cross_project_merge_request]) + end + + it 'excludes cross project references if the user cannot read cross project' do + user = create(:user) + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + other_project = create(:project, :public) + cross_project_merge_request = create(:merge_request, source_project: other_project) + create(:note_on_issue, + noteable: issue, + note: cross_project_merge_request.to_reference(issue.project), + project_id: issue.project.id) + + expect(issue.referenced_merge_requests(user)).to eq([merge_request]) end end @@ -309,7 +337,7 @@ describe Issue do end describe '#related_branches' do - let(:user) { build(:admin) } + let(:user) { create(:admin) } before do allow(subject.project.repository).to receive(:branch_names) diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb new file mode 100644 index 00000000000..eda0e1da835 --- /dev/null +++ b/spec/models/notification_recipient_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe NotificationRecipient do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:target) { create(:issue, project: project) } + + subject(:recipient) { described_class.new(user, :watch, target: target, project: project) } + + it 'denies access to a target when cross project access is denied' do + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(false) + + expect(recipient.has_access?).to be_falsy + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ee04d74d848..56c2d7b953e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1473,6 +1473,13 @@ describe Project do expect(project.user_can_push_to_empty_repo?(user)).to be_truthy end + + it 'returns false when the repo is not empty' do + project.add_master(user) + expect(project).to receive(:empty_repo?).and_return(false) + + expect(project.user_can_push_to_empty_repo?(user)).to be_falsey + end end describe '#container_registry_url' do diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb index 2cf669e8191..d1bf98995e7 100644 --- a/spec/policies/issuable_policy_spec.rb +++ b/spec/policies/issuable_policy_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe IssuablePolicy, models: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:policies) { described_class.new(user, issue) } + describe '#rules' do context 'when discussion is locked for the issuable' do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project, discussion_locked: true) } - let(:policies) { described_class.new(user, issue) } context 'when the user is not a project member' do it 'can not create a note' do diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index a4af9361ea6..793b724bfca 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -30,41 +30,41 @@ describe IssuePolicy do end it 'does not allow non-members to read issues' do - expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows guests to read issues' do - expect(permissions(guest, issue)).to be_allowed(:read_issue) + expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) - expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, issue)).to be_disallowed(:admin_issue) - expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end @@ -73,37 +73,37 @@ describe IssuePolicy do let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } it 'does not allow non-members to read confidential issues' do - expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end end end @@ -123,36 +123,36 @@ describe IssuePolicy do end it 'allows guests to read issues' do - expect(permissions(guest, issue)).to be_allowed(:read_issue) + expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) - expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, issue)).to be_disallowed(:admin_issue) - expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end @@ -161,32 +161,32 @@ describe IssuePolicy do let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporter from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end end end diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 75553afc033..38d84cf0ceb 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -24,7 +24,7 @@ describe MergeRequests::CreateFromIssueService do end it 'delegates issue search to IssuesFinder' do - expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original + expect_any_instance_of(IssuesFinder).to receive(:find_by).once.and_call_original described_class.new(project, user, issue_iid: -1).execute end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 5e6c24f5730..562b89e6767 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -943,7 +943,8 @@ describe TodoService do described_class.new.mark_todos_as_done_by_ids(todo, john_doe) - expect_any_instance_of(TodosFinder).not_to receive(:execute) + # Make sure no TodosFinder is inialized to perform counting + expect(TodosFinder).not_to receive(:new) expect(john_doe.todos_done_count).to eq(1) expect(john_doe.todos_pending_count).to eq(1) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5600c9c6ad5..c0f3366fb52 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -185,6 +185,14 @@ RSpec.configure do |config| config.around(:each, :postgresql) do |example| example.run if Gitlab::Database.postgresql? end + + # This makes sure the `ApplicationController#can?` method is stubbed with the + # original implementation for all view specs. + config.before(:each, type: :view) do + allow(view).to receive(:can?) do |*args| + Ability.allowed?(*args) + end + end end # add simpler way to match asset paths containing digest strings diff --git a/spec/support/snippet_visibility.rb b/spec/support/snippet_visibility.rb index 1cb904823d2..3a7c69b7877 100644 --- a/spec/support/snippet_visibility.rb +++ b/spec/support/snippet_visibility.rb @@ -252,6 +252,15 @@ RSpec.shared_examples 'snippet visibility' do results = described_class.new(user).execute expect(results.include?(snippet)).to eq(outcome) end + + it 'returns no snippets when the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + snippets = described_class.new(user).execute + + expect(snippets).to be_empty + end end end end @@ -298,6 +307,15 @@ RSpec.shared_examples 'snippet visibility' do results = described_class.new(user).execute expect(results.include?(snippet)).to eq(outcome) end + + it 'should return personal snippets when the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + results = described_class.new(user).execute + + expect(results.include?(snippet)).to eq(outcome) + end end end end diff --git a/spec/views/shared/projects/_project.html.haml_spec.rb b/spec/views/shared/projects/_project.html.haml_spec.rb index f0a4f153699..3b14045e61f 100644 --- a/spec/views/shared/projects/_project.html.haml_spec.rb +++ b/spec/views/shared/projects/_project.html.haml_spec.rb @@ -5,6 +5,7 @@ describe 'shared/projects/_project.html.haml' do before do allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) + allow(view).to receive(:can?) { true } end it 'should render creator avatar if project has a creator' do -- cgit v1.2.1 From 08266ba0a14ec296b51cda6b54d1648985a11adf Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 21 Feb 2018 12:50:13 +0100 Subject: Use `Redactor` to hide cross project information Since the redactor can be run on multiple documents at once and query results are stored in the request store. --- .../cross_project_issuable_information_filter.rb | 40 ----------------- lib/banzai/pipeline/post_process_pipeline.rb | 1 - lib/banzai/redactor.rb | 21 ++++++++- ...oss_project_issuable_information_filter_spec.rb | 50 ---------------------- spec/lib/banzai/filter/redactor_filter_spec.rb | 2 +- spec/lib/banzai/redactor_spec.rb | 49 +++++++++++++++++++++ 6 files changed, 70 insertions(+), 93 deletions(-) delete mode 100644 lib/banzai/filter/cross_project_issuable_information_filter.rb delete mode 100644 spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb diff --git a/lib/banzai/filter/cross_project_issuable_information_filter.rb b/lib/banzai/filter/cross_project_issuable_information_filter.rb deleted file mode 100644 index c2c08b4fd6a..00000000000 --- a/lib/banzai/filter/cross_project_issuable_information_filter.rb +++ /dev/null @@ -1,40 +0,0 @@ -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 diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb index 4cf97e2d9d2..dcd52bc03c7 100644 --- a/lib/banzai/pipeline/post_process_pipeline.rb +++ b/lib/banzai/pipeline/post_process_pipeline.rb @@ -6,7 +6,6 @@ module Banzai Filter::RedactorFilter, Filter::RelativeLinkFilter, Filter::IssuableStateFilter, - Filter::CrossProjectIssuableInformationFilter, Filter::AbsoluteLinkFilter ] end diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb index de3ebe72720..827df7c08ae 100644 --- a/lib/banzai/redactor.rb +++ b/lib/banzai/redactor.rb @@ -19,8 +19,9 @@ module Banzai # # Returns the documents passed as the first argument. def redact(documents) - all_document_nodes = document_nodes(documents) + redact_cross_project_references(documents) unless can_read_cross_project? + all_document_nodes = document_nodes(documents) redact_document_nodes(all_document_nodes) end @@ -51,6 +52,18 @@ module Banzai metadata end + def redact_cross_project_references(documents) + extractor = Banzai::IssuableExtractor.new(project, user) + issuables = extractor.extract(documents) + + issuables.each do |node, issuable| + next if issuable.project == project + + node['class'] = node['class'].gsub('has-tooltip', '') + node['title'] = nil + end + end + # Returns the nodes visible to the current user. # # nodes - The input nodes to check. @@ -78,5 +91,11 @@ module Banzai { document: document, nodes: Querying.css(document, 'a.gfm[data-reference-type]') } end end + + private + + def can_read_cross_project? + Ability.allowed?(user, :read_cross_project) + end end end diff --git a/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb b/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb deleted file mode 100644 index 92b8c526e88..00000000000 --- a/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -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 diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index 5a7858e77f3..9a2e521fdcf 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -6,7 +6,7 @@ describe Banzai::Filter::RedactorFilter do it 'ignores non-GFM links' do html = %(See Google) - doc = filter(html, current_user: double) + doc = filter(html, current_user: build(:user)) expect(doc.css('a').length).to eq 1 end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 893a10d1b90..1fa89137972 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -88,6 +88,55 @@ describe Banzai::Redactor do end end + context 'when the user cannot read cross project' do + include ActionView::Helpers::UrlHelper + let(:project) { create(:project) } + let(:other_project) { create(:project, :public) } + + def create_link(issuable) + type = issuable.class.name.underscore.downcase + link_to(issuable.to_reference, '', + class: 'gfm has-tooltip', + title: issuable.title, + data: { + reference_type: type, + "#{type}": issuable.id + }) + end + + before do + project.add_developer(user) + + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global) { false } + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + end + + it 'skips links to issues within the same project' do + issue = create(:issue, project: project) + link = create_link(issue) + doc = Nokogiri::HTML.fragment(link) + + redactor.redact([doc]) + result = doc.css('a').last + + expect(result['class']).to include('has-tooltip') + expect(result['title']).to eq(issue.title) + end + + it 'removes info from a cross project reference' do + issue = create(:issue, project: other_project) + link = create_link(issue) + doc = Nokogiri::HTML.fragment(link) + + redactor.redact([doc]) + result = doc.css('a').last + + expect(result['class']).not_to include('has-tooltip') + expect(result['title']).to be_empty + end + end + describe '#redact_nodes' do it 'redacts an Array of nodes' do doc = Nokogiri::HTML.fragment('foo') -- cgit v1.2.1