summaryrefslogtreecommitdiff
path: root/lib/gitlab
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab')
-rw-r--r--lib/gitlab/checks/change_access.rb48
-rw-r--r--lib/gitlab/ci_access.rb9
-rw-r--r--lib/gitlab/etag_caching/router.rb8
-rw-r--r--lib/gitlab/git/diff.rb30
-rw-r--r--lib/gitlab/git_access.rb92
-rw-r--r--lib/gitlab/git_access_status.rb15
-rw-r--r--lib/gitlab/git_access_wiki.rb12
-rw-r--r--lib/gitlab/i18n.rb5
-rw-r--r--lib/gitlab/otp_key_rotator.rb87
9 files changed, 237 insertions, 69 deletions
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index c984eb20606..b6805230348 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -1,6 +1,20 @@
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 master 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.'
+ }.freeze
+
attr_reader :user_access, :project, :skip_authorization, :protocol
def initialize(
@@ -17,22 +31,20 @@ module Gitlab
end
def exec
- return GitAccessStatus.new(true) if skip_authorization
+ return true if skip_authorization
- error = push_checks || branch_checks || tag_checks
+ push_checks
+ branch_checks
+ tag_checks
- if error
- GitAccessStatus.new(false, error)
- else
- GitAccessStatus.new(true)
- end
+ true
end
protected
def push_checks
if user_access.cannot_do_action?(:push_code)
- "You are not allowed to push code to this project."
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
end
end
@@ -40,7 +52,7 @@ module Gitlab
return unless @branch_name
if deletion? && @branch_name == project.default_branch
- return "The default branch of a project cannot be deleted."
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
end
protected_branch_checks
@@ -50,7 +62,7 @@ module Gitlab
return unless ProtectedBranch.protected?(project, @branch_name)
if forced_push?
- return "You are not allowed to force push code to a protected branch on this project."
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
end
if deletion?
@@ -62,22 +74,22 @@ module Gitlab
def protected_branch_deletion_checks
unless user_access.can_delete_branch?(@branch_name)
- return 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.'
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
end
unless protocol == 'web'
- 'You can only delete protected branches using the web interface.'
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
end
end
def protected_branch_push_checks
if matching_merge_request?
unless user_access.can_merge_to_branch?(@branch_name) || user_access.can_push_to_branch?(@branch_name)
- "You are not allowed to merge code into protected branches on this project."
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
end
else
unless user_access.can_push_to_branch?(@branch_name)
- "You are not allowed to push code to protected branches on this project."
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_protected_branch]
end
end
end
@@ -86,7 +98,7 @@ module Gitlab
return unless @tag_name
if tag_exists? && user_access.cannot_do_action?(:admin_project)
- return "You are not allowed to change existing tags on this project."
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
end
protected_tag_checks
@@ -95,11 +107,11 @@ module Gitlab
def protected_tag_checks
return unless ProtectedTag.protected?(project, @tag_name)
- return "Protected tags cannot be updated." if update?
- return "Protected tags cannot be deleted." if deletion?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
unless user_access.can_create_tag?(@tag_name)
- return "You are not allowed to create this tag as it is protected."
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
end
end
diff --git a/lib/gitlab/ci_access.rb b/lib/gitlab/ci_access.rb
new file mode 100644
index 00000000000..def1373d8cf
--- /dev/null
+++ b/lib/gitlab/ci_access.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ # For backwards compatibility, generic CI (which is a build without a user) is
+ # allowed to :build_download_code without any other checks.
+ class CiAccess
+ def can_do_action?(action)
+ action == :build_download_code
+ end
+ end
+end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index 2f9d8bfc266..ca49eda51fb 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -9,8 +9,8 @@ module Gitlab
# - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
- commit pipelines merge_requests new
- environments].freeze
+ commit pipelines merge_requests builds
+ new environments].freeze
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape)))
@@ -44,6 +44,10 @@ module Gitlab
'project_pipeline'
),
Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/builds/\d+\.json\z),
+ 'project_build'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z),
'environments'
)
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 7e21994a084..8926aa19925 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -20,13 +20,25 @@ module Gitlab
# We need this accessor because of `to_hash` and `init_from_hash`
attr_accessor :too_large
- # The maximum size of a diff to display.
- SIZE_LIMIT = 100.kilobytes
+ class << self
+ # The maximum size of a diff to display.
+ def size_limit
+ if Feature.enabled?('gitlab_git_diff_size_limit_increase')
+ 200.kilobytes
+ else
+ 100.kilobytes
+ end
+ end
- # The maximum size before a diff is collapsed.
- COLLAPSE_LIMIT = 10.kilobytes
+ # The maximum size before a diff is collapsed.
+ def collapse_limit
+ if Feature.enabled?('gitlab_git_diff_size_limit_increase')
+ 100.kilobytes
+ else
+ 10.kilobytes
+ end
+ end
- class << self
def between(repo, head, base, options = {}, *paths)
straight = options.delete(:straight) || false
@@ -231,7 +243,7 @@ module Gitlab
def too_large?
if @too_large.nil?
- @too_large = @diff.bytesize >= SIZE_LIMIT
+ @too_large = @diff.bytesize >= self.class.size_limit
else
@too_large
end
@@ -246,7 +258,7 @@ module Gitlab
def collapsed?
return @collapsed if defined?(@collapsed)
- @collapsed = !expanded && @diff.bytesize >= COLLAPSE_LIMIT
+ @collapsed = !expanded && @diff.bytesize >= self.class.collapse_limit
end
def collapse!
@@ -318,14 +330,14 @@ module Gitlab
hunk.each_line do |line|
size += line.content.bytesize
- if size >= SIZE_LIMIT
+ if size >= self.class.size_limit
too_large!
return true
end
end
end
- if !expanded && size >= COLLAPSE_LIMIT
+ if !expanded && size >= self.class.collapse_limit
collapse!
return true
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 99724db8da2..0a19d24eb20 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -3,33 +3,39 @@
module Gitlab
class GitAccess
UnauthorizedError = Class.new(StandardError)
+ NotFoundError = Class.new(StandardError)
ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.',
deploy_key_upload:
'This deploy key does not have write access to this project.',
- no_repo: 'A repository for this project does not exist yet.'
+ no_repo: 'A repository for this project does not exist yet.',
+ project_not_found: 'The project you were looking for could not be found.',
+ account_blocked: 'Your account has been blocked.',
+ command_not_allowed: "The command you're trying to execute is not allowed.",
+ upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
+ receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.'
}.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
- attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
+ attr_reader :actor, :project, :protocol, :authentication_abilities
def initialize(actor, project, protocol, authentication_abilities:)
@actor = actor
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
- @user_access = UserAccess.new(user, project: project)
end
def check(cmd, changes)
check_protocol!
check_active_user!
check_project_accessibility!
+ check_command_disabled!(cmd)
check_command_existence!(cmd)
check_repository_existence!
@@ -40,9 +46,7 @@ module Gitlab
check_push_access!(changes)
end
- build_status_object(true)
- rescue UnauthorizedError => ex
- build_status_object(false, ex.message)
+ true
end
def guest_can_download_code?
@@ -73,19 +77,39 @@ module Gitlab
return if deploy_key?
if user && !user_access.allowed?
- raise UnauthorizedError, "Your account has been blocked."
+ raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
end
end
def check_project_accessibility!
if project.blank? || !can_read_project?
- raise UnauthorizedError, 'The project you were looking for could not be found.'
+ raise NotFoundError, ERROR_MESSAGES[:project_not_found]
+ end
+ end
+
+ def check_command_disabled!(cmd)
+ if upload_pack?(cmd)
+ check_upload_pack_disabled!
+ elsif receive_pack?(cmd)
+ check_receive_pack_disabled!
+ end
+ end
+
+ def check_upload_pack_disabled!
+ if http? && upload_pack_disabled_over_http?
+ raise UnauthorizedError, ERROR_MESSAGES[:upload_pack_disabled_over_http]
+ end
+ end
+
+ def check_receive_pack_disabled!
+ if http? && receive_pack_disabled_over_http?
+ raise UnauthorizedError, ERROR_MESSAGES[:receive_pack_disabled_over_http]
end
end
def check_command_existence!(cmd)
unless ALL_COMMANDS.include?(cmd)
- raise UnauthorizedError, "The command you're trying to execute is not allowed."
+ raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed]
end
end
@@ -138,11 +162,9 @@ module Gitlab
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change|
- status = check_single_change_access(change)
- unless status.allowed?
- # If user does not have access to make at least one change - cancel all push
- raise UnauthorizedError, status.message
- end
+ # If user does not have access to make at least one change, cancel all
+ # push by allowing the exception to bubble up
+ check_single_change_access(change)
end
end
@@ -168,14 +190,40 @@ module Gitlab
actor.is_a?(DeployKey)
end
+ def ci?
+ actor == :ci
+ end
+
def can_read_project?
- if deploy_key
+ if deploy_key?
deploy_key.has_access_to?(project)
elsif user
user.can?(:read_project, project)
+ elsif ci?
+ true # allow CI (build without a user) for backwards compatibility
end || Guest.can?(:read_project, project)
end
+ def http?
+ protocol == 'http'
+ end
+
+ def upload_pack?(command)
+ command == 'git-upload-pack'
+ end
+
+ def receive_pack?(command)
+ command == 'git-receive-pack'
+ end
+
+ def upload_pack_disabled_over_http?
+ !Gitlab.config.gitlab_shell.upload_pack
+ end
+
+ def receive_pack_disabled_over_http?
+ !Gitlab.config.gitlab_shell.receive_pack
+ end
+
protected
def user
@@ -185,15 +233,19 @@ module Gitlab
case actor
when User
actor
- when DeployKey
- nil
when Key
- actor.user
+ actor.user unless actor.is_a?(DeployKey)
+ when :ci
+ nil
end
end
- def build_status_object(status, message = '')
- Gitlab::GitAccessStatus.new(status, message)
+ def user_access
+ @user_access ||= if ci?
+ CiAccess.new
+ else
+ UserAccess.new(user, project: project)
+ end
end
end
end
diff --git a/lib/gitlab/git_access_status.rb b/lib/gitlab/git_access_status.rb
deleted file mode 100644
index 09bb01be694..00000000000
--- a/lib/gitlab/git_access_status.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Gitlab
- class GitAccessStatus
- attr_accessor :status, :message
- alias_method :allowed?, :status
-
- def initialize(status, message = '')
- @status = status
- @message = message
- end
-
- def to_json(opts = nil)
- { status: @status, message: @message }.to_json(opts)
- end
- end
-end
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 67eaa5e088d..1fe5155c093 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,5 +1,9 @@
module Gitlab
class GitAccessWiki < GitAccess
+ ERROR_MESSAGES = {
+ write_to_wiki: "You are not allowed to write to this project's wiki."
+ }.freeze
+
def guest_can_download_code?
Guest.can?(:download_wiki_code, project)
end
@@ -9,11 +13,11 @@ module Gitlab
end
def check_single_change_access(change)
- if user_access.can_do_action?(:create_wiki)
- build_status_object(true)
- else
- build_status_object(false, "You are not allowed to write to this project's wiki.")
+ unless user_access.can_do_action?(:create_wiki)
+ raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
+
+ true
end
end
end
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index 5ab3eeb3aff..f7ac48f7dbd 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -5,7 +5,10 @@ module Gitlab
AVAILABLE_LANGUAGES = {
'en' => 'English',
'es' => 'Español',
- 'de' => 'Deutsch'
+ 'de' => 'Deutsch',
+ 'zh_CN' => '简体中文',
+ 'zh_HK' => '繁體中文(香港)',
+ 'zh_TW' => '繁體中文(臺灣)'
}.freeze
def available_locales
diff --git a/lib/gitlab/otp_key_rotator.rb b/lib/gitlab/otp_key_rotator.rb
new file mode 100644
index 00000000000..0d541935bc6
--- /dev/null
+++ b/lib/gitlab/otp_key_rotator.rb
@@ -0,0 +1,87 @@
+module Gitlab
+ # The +otp_key_base+ param is used to encrypt the User#otp_secret attribute.
+ #
+ # When +otp_key_base+ is changed, it invalidates the current encrypted values
+ # of User#otp_secret. This class can be used to decrypt all the values with
+ # the old key, encrypt them with the new key, and and update the database
+ # with the new values.
+ #
+ # For persistence between runs, a CSV file is used with the following columns:
+ #
+ # user_id, old_value, new_value
+ #
+ # Only the encrypted values are stored in this file.
+ #
+ # As users may have their 2FA settings changed at any time, this is only
+ # guaranteed to be safe if run offline.
+ class OtpKeyRotator
+ HEADERS = %w[user_id old_value new_value].freeze
+
+ attr_reader :filename
+
+ # Create a new rotator. +filename+ is used to store values by +calculate!+,
+ # and to update the database with new and old values in +apply!+ and
+ # +rollback!+, respectively.
+ def initialize(filename)
+ @filename = filename
+ end
+
+ def rotate!(old_key:, new_key:)
+ old_key ||= Gitlab::Application.secrets.otp_key_base
+
+ raise ArgumentError.new("Old key is the same as the new key") if old_key == new_key
+ raise ArgumentError.new("New key is too short! Must be 256 bits") if new_key.size < 64
+
+ write_csv do |csv|
+ ActiveRecord::Base.transaction do
+ User.with_two_factor.in_batches do |relation|
+ rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt)
+ rows.each do |row|
+ user = %i[id ciphertext iv salt].zip(row).to_h
+ new_value = reencrypt(user, old_key, new_key)
+
+ User.where(id: user[:id]).update_all(encrypted_otp_secret: new_value)
+ csv << [user[:id], user[:ciphertext], new_value]
+ end
+ end
+ end
+ end
+ end
+
+ def rollback!
+ ActiveRecord::Base.transaction do
+ CSV.foreach(filename, headers: HEADERS, return_headers: false) do |row|
+ User.where(id: row['user_id']).update_all(encrypted_otp_secret: row['old_value'])
+ end
+ end
+ end
+
+ private
+
+ attr_reader :old_key, :new_key
+
+ def otp_secret_settings
+ @otp_secret_settings ||= User.encrypted_attributes[:otp_secret]
+ end
+
+ def reencrypt(user, old_key, new_key)
+ original = user[:ciphertext].unpack("m").join
+ opts = {
+ iv: user[:iv].unpack("m").join,
+ salt: user[:salt].unpack("m").join,
+ algorithm: otp_secret_settings[:algorithm],
+ insecure_mode: otp_secret_settings[:insecure_mode]
+ }
+
+ decrypted = Encryptor.decrypt(original, opts.merge(key: old_key))
+ encrypted = Encryptor.encrypt(decrypted, opts.merge(key: new_key))
+ [encrypted].pack("m")
+ end
+
+ def write_csv(&blk)
+ File.open(filename, "w") do |file|
+ yield CSV.new(file, headers: HEADERS, write_headers: false)
+ end
+ end
+ end
+end