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

Port `read_cross_project` ability from EE

parent b5306075
No related branches found
No related tags found
No related merge requests found
Showing
with 713 additions and 13 deletions
Loading
Loading
@@ -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)
Loading
Loading
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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -6,6 +6,7 @@ module Banzai
Filter::RedactorFilter,
Filter::RelativeLinkFilter,
Filter::IssuableStateFilter,
Filter::CrossProjectIssuableInformationFilter,
Filter::AbsoluteLinkFilter
]
end
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
 
Loading
Loading
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
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
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
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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-07 11:38-0600\n"
"PO-Revision-Date: 2018-02-07 11:38-0600\n"
"POT-Creation-Date: 2018-02-20 10:26+0100\n"
"PO-Revision-Date: 2018-02-20 10:26+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -1033,6 +1084,9 @@ msgstr ""
msgid "Create merge request"
msgstr ""
 
msgid "Create merge request and branch"
msgstr ""
msgid "Create new branch"
msgstr ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -2478,6 +2589,9 @@ msgstr ""
msgid "Source"
msgstr ""
 
msgid "Source (branch or tag)"
msgstr ""
msgid "Source code"
msgstr ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -2934,9 +3051,6 @@ msgstr ""
msgid "Trigger this manual action"
msgstr ""
 
msgid "Type %{value} to confirm:"
msgstr ""
msgid "Unable to reset project cache."
msgstr ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
@@ -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 ""
 
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
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
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment