Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.0 on 2022-01-18 at 14:00 UTC+1 (a few minutes of down time)

Commit 9318c5b7 authored by Douwe Maan's avatar Douwe Maan
Browse files

Merge branch 'ce-fj-refactor-change-access' into 'master'

CE port Refactor Gitlab::Checks::ChangeAccess class

See merge request gitlab-org/gitlab-ce!23533
# frozen_string_literal: true
module Gitlab
module Checks
class BaseChecker
include Gitlab::Utils::StrongMemoize
attr_reader :change_access
delegate(*ChangeAccess::ATTRIBUTES, to: :change_access)
def initialize(change_access)
@change_access = change_access
end
def validate!
raise NotImplementedError
end
private
def deletion?
Gitlab::Git.blank_ref?(newrev)
end
def update?
!Gitlab::Git.blank_ref?(oldrev) && !deletion?
end
def updated_from_web?
protocol == 'web'
end
def tag_exists?
project.repository.tag_exists?(tag_name)
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Checks
class BranchCheck < BaseChecker
ERROR_MESSAGES = {
delete_default_branch: 'The default branch of a project cannot be deleted.',
force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.',
non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.',
non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.',
merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.',
push_protected_branch: 'You are not allowed to push code to protected branches on this project.'
}.freeze
LOG_MESSAGES = {
delete_default_branch_check: "Checking if default branch is being deleted...",
protected_branch_checks: "Checking if you are force pushing to a protected branch...",
protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...",
protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch..."
}.freeze
def validate!
return unless branch_name
logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
if deletion? && branch_name == project.default_branch
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
end
end
protected_branch_checks
end
private
def protected_branch_checks
logger.log_timed(LOG_MESSAGES[:protected_branch_checks]) do
return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
if forced_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
end
end
if deletion?
protected_branch_deletion_checks
else
protected_branch_push_checks
end
end
def protected_branch_deletion_checks
logger.log_timed(LOG_MESSAGES[:protected_branch_deletion_checks]) do
unless user_access.can_delete_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
end
unless updated_from_web?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
end
end
end
def protected_branch_push_checks
logger.log_timed(LOG_MESSAGES[:protected_branch_push_checks]) do
if matching_merge_request?
unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
end
else
unless user_access.can_push_to_branch?(branch_name)
raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
end
end
end
end
def push_to_protected_branch_rejected_message
if project.empty_repo?
empty_project_push_message
else
ERROR_MESSAGES[:push_protected_branch]
end
end
def empty_project_push_message
<<~MESSAGE
A default branch (e.g. master) does not yet exist for #{project.full_path}
Ask a project Owner or Maintainer to create a default branch:
#{project_members_url}
MESSAGE
end
def project_members_url
Gitlab::Routing.url_helpers.project_project_members_url(project)
end
def matching_merge_request?
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
def forced_push?
Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev)
end
end
end
end
......@@ -3,35 +3,11 @@
module Gitlab
module Checks
class ChangeAccess
ERROR_MESSAGES = {
push_code: 'You are not allowed to push code to this project.',
delete_default_branch: 'The default branch of a project cannot be deleted.',
force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.',
non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.',
non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.',
merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.',
push_protected_branch: 'You are not allowed to push code to protected branches on this project.',
change_existing_tags: 'You are not allowed to change existing tags on this project.',
update_protected_tag: 'Protected tags cannot be updated.',
delete_protected_tag: 'Protected tags cannot be deleted.',
create_protected_tag: 'You are not allowed to create this tag as it is protected.',
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
ATTRIBUTES = %i[user_access project skip_authorization
skip_lfs_integrity_check protocol oldrev newrev ref
branch_name tag_name logger commits].freeze
LOG_MESSAGES = {
push_checks: "Checking if you are allowed to push...",
delete_default_branch_check: "Checking if default branch is being deleted...",
protected_branch_checks: "Checking if you are force pushing to a protected branch...",
protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...",
protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch...",
tag_checks: "Checking if you are allowed to change existing tags...",
protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag...",
lfs_objects_exist_check: "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...",
commits_check_file_paths_validation: "Validating commits' file paths...",
commits_check: "Validating commit contents..."
}.freeze
attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name, :logger
attr_reader(*ATTRIBUTES)
def initialize(
change, user_access:, project:, skip_authorization: false,
......@@ -50,206 +26,32 @@ def initialize(
@logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end
def exec(skip_commits_check: false)
def exec
return true if skip_authorization
push_checks
branch_checks
tag_checks
lfs_objects_exist_check unless skip_lfs_integrity_check
commits_check unless skip_commits_check
ref_level_checks
# Check of commits should happen as the last step
# given they're expensive in terms of performance
commits_check
true
end
protected
def push_checks
logger.log_timed(LOG_MESSAGES[__method__]) do
unless can_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
end
end
end
def branch_checks
return unless branch_name
logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
if deletion? && branch_name == project.default_branch
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
end
end
protected_branch_checks
end
def protected_branch_checks
logger.log_timed(LOG_MESSAGES[__method__]) do
return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
if forced_push?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
end
end
if deletion?
protected_branch_deletion_checks
else
protected_branch_push_checks
end
end
def protected_branch_deletion_checks
logger.log_timed(LOG_MESSAGES[__method__]) do
unless user_access.can_delete_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
end
unless updated_from_web?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
end
end
end
def protected_branch_push_checks
logger.log_timed(LOG_MESSAGES[__method__]) do
if matching_merge_request?
unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
end
else
unless user_access.can_push_to_branch?(branch_name)
raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
end
end
end
end
def tag_checks
return unless tag_name
logger.log_timed(LOG_MESSAGES[__method__]) do
if tag_exists? && user_access.cannot_do_action?(:admin_project)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
end
end
protected_tag_checks
def commits
@commits ||= project.repository.new_commits(newrev)
end
def protected_tag_checks
logger.log_timed(LOG_MESSAGES[__method__]) do
return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
protected
unless user_access.can_create_tag?(tag_name)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
end
end
def ref_level_checks
Gitlab::Checks::PushCheck.new(self).validate!
Gitlab::Checks::BranchCheck.new(self).validate!
Gitlab::Checks::TagCheck.new(self).validate!
Gitlab::Checks::LfsCheck.new(self).validate!
end
def commits_check
return if deletion? || newrev.nil?
return unless should_run_commit_validations?
logger.log_timed(LOG_MESSAGES[__method__]) do
# n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
::Gitlab::GitalyClient.allow_n_plus_1_calls do
commits.each do |commit|
logger.check_timeout_reached
commit_check.validate(commit, validations_for_commit(commit))
end
end
end
logger.log_timed(LOG_MESSAGES[:commits_check_file_paths_validation]) do
commit_check.validate_file_paths
end
end
# Method overwritten in EE to inject custom validations
def validations_for_commit(_)
[]
end
private
def push_to_protected_branch_rejected_message
if project.empty_repo?
empty_project_push_message
else
ERROR_MESSAGES[:push_protected_branch]
end
end
def empty_project_push_message
<<~MESSAGE
A default branch (e.g. master) does not yet exist for #{project.full_path}
Ask a project Owner or Maintainer to create a default branch:
#{project_members_url}
MESSAGE
end
def project_members_url
Gitlab::Routing.url_helpers.project_project_members_url(project)
end
def should_run_commit_validations?
commit_check.validate_lfs_file_locks?
end
def updated_from_web?
protocol == 'web'
end
def tag_exists?
project.repository.tag_exists?(tag_name)
end
def forced_push?
Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev)
end
def update?
!Gitlab::Git.blank_ref?(oldrev) && !deletion?
end
def deletion?
Gitlab::Git.blank_ref?(newrev)
end
def matching_merge_request?
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
def lfs_objects_exist_check
logger.log_timed(LOG_MESSAGES[__method__]) do
lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
if lfs_check.objects_missing?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
end
end
end
def commit_check
@commit_check ||= Gitlab::Checks::CommitCheck.new(project, user_access.user, newrev, oldrev)
end
def commits
@commits ||= project.repository.new_commits(newrev)
end
def can_push?
user_access.can_do_action?(:push_code) ||
user_access.can_push_to_branch?(branch_name)
Gitlab::Checks::DiffCheck.new(self).validate!
end
end
end
......
......@@ -2,53 +2,91 @@
module Gitlab
module Checks
class CommitCheck
class DiffCheck < BaseChecker
include Gitlab::Utils::StrongMemoize
attr_reader :project, :user, :newrev, :oldrev
LOG_MESSAGES = {
validate_file_paths: "Validating diffs' file paths...",
diff_content_check: "Validating diff contents..."
}.freeze
def initialize(project, user, newrev, oldrev)
@project = project
@user = user
@newrev = newrev
@oldrev = oldrev
@file_paths = []
def validate!
return unless should_run_diff_validations?
return if commits.empty?
return unless uses_raw_delta_validations?
file_paths = []
process_raw_deltas do |diff|
file_paths << (diff.new_path || diff.old_path)
validate_diff(diff)
end
validate_file_paths(file_paths)
end
def validate(commit, validations)
return if validations.empty? && path_validations.empty?
private
commit.raw_deltas.each do |diff|
@file_paths << (diff.new_path || diff.old_path)
def should_run_diff_validations?
newrev && oldrev && !deletion? && validate_lfs_file_locks?
end
validations.each do |validation|
if error = validation.call(diff)
raise ::Gitlab::GitAccess::UnauthorizedError, error
end
end
def validate_lfs_file_locks?
strong_memoize(:validate_lfs_file_locks) do
project.lfs_enabled? && project.any_lfs_file_locks?
end
end
def validate_file_paths
path_validations.each do |validation|
if error = validation.call(@file_paths)
def uses_raw_delta_validations?
validations_for_diff.present? || path_validations.present?
end
def validate_diff(diff)
validations_for_diff.each do |validation|
if error = validation.call(diff)
raise ::Gitlab::GitAccess::UnauthorizedError, error
end
end
end
def validate_lfs_file_locks?
strong_memoize(:validate_lfs_file_locks) do
project.lfs_enabled? && newrev && oldrev && project.any_lfs_file_locks?
# Method overwritten in EE to inject custom validations
def validations_for_diff
[]
end
def path_validations
validate_lfs_file_locks? ? [lfs_file_locks_validation] : []
end
def process_raw_deltas
logger.log_timed(LOG_MESSAGES[:diff_content_check]) do
# n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
::Gitlab::GitalyClient.allow_n_plus_1_calls do
commits.each do |commit|
logger.check_timeout_reached
commit.raw_deltas.each do |diff|
yield(diff)
end
end
end
end
end
private
def validate_file_paths(file_paths)
logger.log_timed(LOG_MESSAGES[__method__]) do
path_validations.each do |validation|
if error = validation.call(file_paths)
raise ::Gitlab::GitAccess::UnauthorizedError, error
end
end
end
end
# rubocop: disable CodeReuse/ActiveRecord
def lfs_file_locks_validation
lambda do |paths|
lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user.id).first
lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user_access.user.id).take
if lfs_lock
return "The path '#{lfs_lock.path}' is locked in Git LFS by #{lfs_lock.user.name}"
......@@ -56,10 +94,6 @@ def lfs_file_locks_validation
end
end
# rubocop: enable CodeReuse/ActiveRecord
def path_validations
validate_lfs_file_locks? ? [lfs_file_locks_validation] : []
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Checks
class LfsCheck < BaseChecker
LOG_MESSAGE = "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...".freeze
ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'.freeze
def validate!
return if skip_lfs_integrity_check
logger.log_timed(LOG_MESSAGE) do
lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
if lfs_check.objects_missing?
raise GitAccess::UnauthorizedError, ERROR_MESSAGE
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Checks
class PushCheck < BaseChecker
def validate!
logger.log_timed("Checking if you are allowed to push...") do
unless can_push?
raise GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.'
end
end
end
private
def can_push?
user_access.can_do_action?(:push_code) ||
user_access.can_push_to_branch?(branch_name)
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Checks
class TagCheck < BaseChecker
ERROR_MESSAGES = {
change_existing_tags: 'You are not allowed to change existing tags on this project.',
update_protected_tag: 'Protected tags cannot be updated.',
delete_protected_tag: 'Protected tags cannot be deleted.',
create_protected_tag: 'You are not allowed to create this tag as it is protected.'
}.freeze
LOG_MESSAGES = {
tag_checks: "Checking if you are allowed to change existing tags...",
protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag..."
}.freeze
def validate!
return unless tag_name
logger.log_timed(LOG_MESSAGES[:tag_checks]) do
if tag_exists? && user_access.cannot_do_action?(:admin_project)
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
end
end
protected_tag_checks
end
private