summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/settings.rb2
-rw-r--r--lib/banzai/filter/cross_project_issuable_information_filter.rb40
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb6
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb1
-rw-r--r--lib/banzai/reference_parser/issuable_parser.rb2
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb25
-rw-r--r--lib/gitlab/contributions_calendar.rb6
-rw-r--r--lib/gitlab/cross_project_access.rb67
-rw-r--r--lib/gitlab/cross_project_access/check_collection.rb47
-rw-r--r--lib/gitlab/cross_project_access/check_info.rb66
-rw-r--r--lib/gitlab/cross_project_access/class_methods.rb48
-rw-r--r--lib/gitlab/user_access.rb2
14 files changed, 308 insertions, 8 deletions
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