summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorNick Thomas <nick@gitlab.com>2018-08-14 00:22:46 +0100
committerNick Thomas <nick@gitlab.com>2018-08-14 00:22:46 +0100
commitc8bf2e7d47c3b8f34cb79847edcd5dd50b8f280e (patch)
treecc22dc6c91f58ccaadd97fdd816159de6ec8a135 /lib
parent764f6f47fa6a8698ae033532ae49875a87030518 (diff)
downloadgitlab-shell-c8bf2e7d47c3b8f34cb79847edcd5dd50b8f280e.tar.gz
Revert "Merge branch 'ash.mckenzie/srp-refactor' into 'master'"
This reverts commit 3aaf4751e09262c53544a1987f59b1308af9b6c1, reversing changes made to c6577e0d75f51b017f2f332838b97c3ca5b497c0.
Diffstat (limited to 'lib')
-rw-r--r--lib/action.rb7
-rw-r--r--lib/action/api_2fa_recovery.rb54
-rw-r--r--lib/action/base.rb27
-rw-r--r--lib/action/git_lfs_authenticate.rb26
-rw-r--r--lib/action/gitaly.rb126
-rw-r--r--lib/actor.rb21
-rw-r--r--lib/actor/base.rb56
-rw-r--r--lib/actor/key.rb29
-rw-r--r--lib/actor/user.rb19
-rw-r--r--lib/actor/username.rb25
-rw-r--r--lib/errors.rb4
-rw-r--r--lib/gitlab_access.rb36
-rw-r--r--lib/gitlab_access_status.rb32
-rw-r--r--lib/gitlab_custom_hook.rb4
-rw-r--r--lib/gitlab_keys.rb2
-rw-r--r--lib/gitlab_net.rb149
-rw-r--r--lib/gitlab_post_receive.rb18
-rw-r--r--lib/gitlab_shell.rb307
-rw-r--r--lib/http_helper.rb19
19 files changed, 397 insertions, 564 deletions
diff --git a/lib/action.rb b/lib/action.rb
deleted file mode 100644
index 1f9cc6c..0000000
--- a/lib/action.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require_relative 'action/base'
-require_relative 'action/gitaly'
-require_relative 'action/git_lfs_authenticate'
-require_relative 'action/api_2fa_recovery'
-
-module Action
-end
diff --git a/lib/action/api_2fa_recovery.rb b/lib/action/api_2fa_recovery.rb
deleted file mode 100644
index 06f8057..0000000
--- a/lib/action/api_2fa_recovery.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require_relative '../action'
-require_relative '../gitlab_logger'
-
-module Action
- class API2FARecovery < Base
- def initialize(actor)
- @actor = actor
- end
-
- def execute(_, _)
- recover
- end
-
- private
-
- attr_reader :actor
-
- def continue?(question)
- puts "#{question} (yes/no)"
- STDOUT.flush # Make sure the question gets output before we wait for input
- response = STDIN.gets.chomp
- puts '' # Add a buffer in the output
- response == 'yes'
- end
-
- def recover
- continue = continue?(
- "Are you sure you want to generate new two-factor recovery codes?\n" \
- "Any existing recovery codes you saved will be invalidated."
- )
-
- unless continue
- puts 'New recovery codes have *not* been generated. Existing codes will remain valid.'
- return
- end
-
- resp = api.two_factor_recovery_codes(actor)
- if resp['success']
- codes = resp['recovery_codes'].join("\n")
- $logger.info('API 2FA recovery success', user: actor.log_username)
- puts "Your two-factor authentication recovery codes are:\n\n" \
- "#{codes}\n\n" \
- "During sign in, use one of the codes above when prompted for\n" \
- "your two-factor code. Then, visit your Profile Settings and add\n" \
- "a new device so you do not lose access to your account again."
- true
- else
- $logger.info('API 2FA recovery error', user: actor.log_username)
- puts "An error occurred while trying to generate new recovery codes.\n" \
- "#{resp['message']}"
- end
- end
- end
-end
diff --git a/lib/action/base.rb b/lib/action/base.rb
deleted file mode 100644
index fe8c836..0000000
--- a/lib/action/base.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'json'
-
-require_relative '../gitlab_config'
-require_relative '../gitlab_net'
-require_relative '../gitlab_metrics'
-
-module Action
- class Base
- def initialize
- raise NotImplementedError
- end
-
- def self.create_from_json(_)
- raise NotImplementedError
- end
-
- private
-
- def config
- @config ||= GitlabConfig.new
- end
-
- def api
- @api ||= GitlabNet.new
- end
- end
-end
diff --git a/lib/action/git_lfs_authenticate.rb b/lib/action/git_lfs_authenticate.rb
deleted file mode 100644
index 8c5294d..0000000
--- a/lib/action/git_lfs_authenticate.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require_relative '../action'
-require_relative '../gitlab_logger'
-
-module Action
- class GitLFSAuthenticate < Base
- def initialize(actor, repo_name)
- @actor = actor
- @repo_name = repo_name
- end
-
- def execute(_, _)
- GitlabMetrics.measure('lfs-authenticate') do
- $logger.info('Processing LFS authentication', user: actor.log_username)
- lfs_access = api.lfs_authenticate(actor, repo_name)
- return unless lfs_access
-
- puts lfs_access.authentication_payload
- end
- true
- end
-
- private
-
- attr_reader :actor, :repo_name
- end
-end
diff --git a/lib/action/gitaly.rb b/lib/action/gitaly.rb
deleted file mode 100644
index b95ff17..0000000
--- a/lib/action/gitaly.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-require_relative '../action'
-require_relative '../gitlab_logger'
-require_relative '../gitlab_net'
-
-module Action
- class Gitaly < Base
- REPOSITORY_PATH_NOT_PROVIDED = "Repository path not provided. Please make sure you're using GitLab v8.10 or later.".freeze
- MIGRATED_COMMANDS = {
- 'git-upload-pack' => File.join(ROOT_PATH, 'bin', 'gitaly-upload-pack'),
- 'git-upload-archive' => File.join(ROOT_PATH, 'bin', 'gitaly-upload-archive'),
- 'git-receive-pack' => File.join(ROOT_PATH, 'bin', 'gitaly-receive-pack')
- }.freeze
-
- def initialize(actor, gl_repository, gl_username, git_protocol, repository_path, gitaly)
- @actor = actor
- @gl_repository = gl_repository
- @gl_username = gl_username
- @git_protocol = git_protocol
- @repository_path = repository_path
- @gitaly = gitaly
- end
-
- def self.create_from_json(actor, json)
- new(actor,
- json['gl_repository'],
- json['gl_username'],
- json['git_protocol'],
- json['repository_path'],
- json['gitaly'])
- end
-
- def execute(command, args)
- raise ArgumentError, REPOSITORY_PATH_NOT_PROVIDED unless repository_path
- raise InvalidRepositoryPathError unless valid_repository?
-
- $logger.info('Performing Gitaly command', user: actor.log_username)
- process(command, args)
- end
-
- private
-
- attr_reader :actor, :gl_repository, :gl_username, :repository_path, :gitaly
-
- def git_protocol
- @git_protocol || ENV['GIT_PROTOCOL'] # TODO: tidy this up
- end
-
- def process(command, args)
- executable = command
- args = [repository_path]
-
- if MIGRATED_COMMANDS.key?(executable) && gitaly
- executable = MIGRATED_COMMANDS[executable]
- gitaly_address = gitaly['address']
- args = [gitaly_address, JSON.dump(gitaly_request)]
- end
-
- args_string = [File.basename(executable), *args].join(' ')
- $logger.info('executing git command', command: args_string, user: actor.log_username)
-
- exec_cmd(executable, *args)
- end
-
- def exec_cmd(*args)
- env = exec_env
- env['GITALY_TOKEN'] = gitaly['token'] if gitaly && gitaly.include?('token')
-
- if git_trace_available?
- env.merge!(
- 'GIT_TRACE' => config.git_trace_log_file,
- 'GIT_TRACE_PACKET' => config.git_trace_log_file,
- 'GIT_TRACE_PERFORMANCE' => config.git_trace_log_file
- )
- end
-
- # We use 'chdir: ROOT_PATH' to let the next executable know where config.yml is.
- Kernel.exec(env, *args, unsetenv_others: true, chdir: ROOT_PATH)
- end
-
- def exec_env
- {
- 'HOME' => ENV['HOME'],
- 'PATH' => ENV['PATH'],
- 'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
- 'LANG' => ENV['LANG'],
- 'GL_ID' => actor.identifier,
- 'GL_PROTOCOL' => GitlabNet::GL_PROTOCOL,
- 'GL_REPOSITORY' => gl_repository,
- 'GL_USERNAME' => gl_username
- }
- end
-
- def gitaly_request
- # The entire gitaly_request hash should be built in gitlab-ce and passed
- # on as-is. For now we build a fake one on the spot.
- {
- 'repository' => gitaly['repository'],
- 'gl_repository' => gl_repository,
- 'gl_id' => actor.identifier,
- 'gl_username' => gl_username,
- 'git_protocol' => git_protocol
- }
- end
-
- def valid_repository?
- File.absolute_path(repository_path) == repository_path
- end
-
- def git_trace_available?
- return false unless config.git_trace_log_file
-
- if Pathname(config.git_trace_log_file).relative?
- $logger.warn('git trace log path must be absolute, ignoring', git_trace_log_file: config.git_trace_log_file)
- return false
- end
-
- begin
- File.open(config.git_trace_log_file, 'a') { nil }
- return true
- rescue => ex
- $logger.warn('Failed to open git trace log file', git_trace_log_file: config.git_trace_log_file, error: ex.to_s)
- return false
- end
- end
- end
-end
diff --git a/lib/actor.rb b/lib/actor.rb
deleted file mode 100644
index 4e8b3b8..0000000
--- a/lib/actor.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require_relative 'actor/base'
-require_relative 'actor/key'
-require_relative 'actor/user'
-require_relative 'actor/username'
-
-module Actor
- class UnsupportedActorError < StandardError; end
-
- def self.new_from(str, audit_usernames: false)
- case str
- when Key.id_regex
- Key.from(str, audit_usernames: audit_usernames)
- when User.id_regex
- User.from(str, audit_usernames: audit_usernames)
- when Username.id_regex
- Username.from(str, audit_usernames: audit_usernames)
- else
- raise UnsupportedActorError
- end
- end
-end
diff --git a/lib/actor/base.rb b/lib/actor/base.rb
deleted file mode 100644
index 0510c60..0000000
--- a/lib/actor/base.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-module Actor
- class Base
- attr_reader :id
-
- def initialize(id, audit_usernames: false)
- @id = id
- @audit_usernames = audit_usernames
- end
-
- def self.from(str, audit_usernames: false)
- new(str.gsub(/#{identifier_prefix}-/, ''), audit_usernames: audit_usernames)
- end
-
- def self.identifier_key
- raise NotImplementedError
- end
-
- def self.identifier_prefix
- raise NotImplementedError
- end
-
- def self.id_regex
- raise NotImplementedError
- end
-
- def username
- raise NotImplementedError
- end
-
- def identifier
- "#{self.class.identifier_prefix}-#{id}"
- end
-
- def identifier_key
- self.class.identifier_key
- end
-
- def log_username
- audit_usernames? ? username : "#{label} with identifier #{identifier}"
- end
-
- private
-
- attr_reader :audit_usernames
-
- alias audit_usernames? audit_usernames
-
- def klass_name
- self.class.to_s.split('::')[-1]
- end
-
- def label
- klass_name.downcase
- end
- end
-end
diff --git a/lib/actor/key.rb b/lib/actor/key.rb
deleted file mode 100644
index 46f013a..0000000
--- a/lib/actor/key.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require_relative 'base'
-require_relative '../gitlab_net'
-
-module Actor
- class Key < Base
- ANONYMOUS_USER = 'Anonymous'.freeze
-
- alias key_id id
-
- def self.identifier_prefix
- 'key'.freeze
- end
-
- def self.identifier_key
- 'key_id'.freeze
- end
-
- def self.id_regex
- /\Akey\-\d+\Z/
- end
-
- def username
- @username ||= begin
- user = GitlabNet.new.discover(self)
- user ? "@#{user['username']}" : ANONYMOUS_USER
- end
- end
- end
-end
diff --git a/lib/actor/user.rb b/lib/actor/user.rb
deleted file mode 100644
index 55ba7f1..0000000
--- a/lib/actor/user.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require_relative 'base'
-
-module Actor
- class User < Base
- alias username identifier
-
- def self.identifier_prefix
- 'user'.freeze
- end
-
- def self.identifier_key
- 'user_id'.freeze
- end
-
- def self.id_regex
- /\Auser\-\d+\Z/
- end
- end
-end
diff --git a/lib/actor/username.rb b/lib/actor/username.rb
deleted file mode 100644
index cd9d6e5..0000000
--- a/lib/actor/username.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require_relative 'base'
-require_relative 'key'
-
-module Actor
- class Username < Key
- def self.identifier_prefix
- 'username'.freeze
- end
-
- def self.identifier_key
- 'username'.freeze
- end
-
- def self.id_regex
- /\Ausername\-[a-z0-9-]+\z/
- end
-
- private
-
- # Override Base#label
- def label
- 'user'
- end
- end
-end
diff --git a/lib/errors.rb b/lib/errors.rb
deleted file mode 100644
index 7bef3b4..0000000
--- a/lib/errors.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-class UnknownError < StandardError; end
-class AccessDeniedError < StandardError; end
-class InvalidRepositoryPathError < StandardError; end
-class DisallowedCommandError < StandardError; end
diff --git a/lib/gitlab_access.rb b/lib/gitlab_access.rb
index 6683ee7..e1a5e35 100644
--- a/lib/gitlab_access.rb
+++ b/lib/gitlab_access.rb
@@ -1,28 +1,34 @@
-require 'json'
-
-require_relative 'errors'
-require_relative 'actor'
require_relative 'gitlab_init'
require_relative 'gitlab_net'
+require_relative 'gitlab_access_status'
require_relative 'names_helper'
require_relative 'gitlab_metrics'
require_relative 'object_dirs_helper'
+require 'json'
class GitlabAccess
+ class AccessDeniedError < StandardError; end
+
include NamesHelper
- def initialize(gl_repository, repo_path, gl_id, changes, protocol)
+ attr_reader :config, :gl_repository, :repo_path, :changes, :protocol
+
+ def initialize(gl_repository, repo_path, actor, changes, protocol)
+ @config = GitlabConfig.new
@gl_repository = gl_repository
@repo_path = repo_path.strip
- @gl_id = gl_id
+ @actor = actor
@changes = changes.lines
@protocol = protocol
end
def exec
- GitlabMetrics.measure('check-access:git-receive-pack') do
- api.check_access('git-receive-pack', gl_repository, repo_path, actor, changes, protocol, env: ObjectDirsHelper.all_attributes.to_json)
+ status = GitlabMetrics.measure('check-access:git-receive-pack') do
+ api.check_access('git-receive-pack', @gl_repository, @repo_path, @actor, @changes, @protocol, env: ObjectDirsHelper.all_attributes.to_json)
end
+
+ raise AccessDeniedError, status.message unless status.allowed?
+
true
rescue GitlabNet::ApiUnreachableError
$stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
@@ -32,19 +38,9 @@ class GitlabAccess
false
end
- private
-
- attr_reader :gl_repository, :repo_path, :gl_id, :changes, :protocol
+ protected
def api
- @api ||= GitlabNet.new
- end
-
- def config
- @config ||= GitlabConfig.new
- end
-
- def actor
- @actor ||= Actor.new_from(gl_id, audit_usernames: config.audit_usernames)
+ GitlabNet.new
end
end
diff --git a/lib/gitlab_access_status.rb b/lib/gitlab_access_status.rb
new file mode 100644
index 0000000..c639462
--- /dev/null
+++ b/lib/gitlab_access_status.rb
@@ -0,0 +1,32 @@
+require 'json'
+
+class GitAccessStatus
+ attr_reader :message, :gl_repository, :gl_id, :gl_username, :repository_path, :gitaly, :git_protocol
+
+ def initialize(status, message, gl_repository:, gl_id:, gl_username:, repository_path:, gitaly:, git_protocol:)
+ @status = status
+ @message = message
+ @gl_repository = gl_repository
+ @gl_id = gl_id
+ @gl_username = gl_username
+ @repository_path = repository_path
+ @gitaly = gitaly
+ @git_protocol = git_protocol
+ end
+
+ def self.create_from_json(json)
+ values = JSON.parse(json)
+ new(values["status"],
+ values["message"],
+ gl_repository: values["gl_repository"],
+ gl_id: values["gl_id"],
+ gl_username: values["gl_username"],
+ repository_path: values["repository_path"],
+ gitaly: values["gitaly"],
+ git_protocol: values["git_protocol"])
+ end
+
+ def allowed?
+ @status
+ end
+end
diff --git a/lib/gitlab_custom_hook.rb b/lib/gitlab_custom_hook.rb
index de6eb38..67096df 100644
--- a/lib/gitlab_custom_hook.rb
+++ b/lib/gitlab_custom_hook.rb
@@ -5,9 +5,9 @@ require_relative 'gitlab_metrics'
class GitlabCustomHook
attr_reader :vars, :config
- def initialize(repo_path, gl_id)
+ def initialize(repo_path, key_id)
@repo_path = repo_path
- @vars = { 'GL_ID' => gl_id }
+ @vars = { 'GL_ID' => key_id }
@config = GitlabConfig.new
end
diff --git a/lib/gitlab_keys.rb b/lib/gitlab_keys.rb
index d0463e2..3ee2882 100644
--- a/lib/gitlab_keys.rb
+++ b/lib/gitlab_keys.rb
@@ -9,7 +9,6 @@ class GitlabKeys # rubocop:disable Metrics/ClassLength
attr_accessor :auth_file, :key
- # TODO: whatever is not a great name
def self.command(whatever)
"#{ROOT_PATH}/bin/gitlab-shell #{whatever}"
end
@@ -22,7 +21,6 @@ class GitlabKeys # rubocop:disable Metrics/ClassLength
command(key_id)
end
- # TODO: whatever is not a great name
def self.whatever_line(command, trailer)
"command=\"#{command}\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty #{trailer}"
end
diff --git a/lib/gitlab_net.rb b/lib/gitlab_net.rb
index 9ea18aa..9cb7e56 100644
--- a/lib/gitlab_net.rb
+++ b/lib/gitlab_net.rb
@@ -1,20 +1,23 @@
+require 'net/http'
+require 'openssl'
require 'json'
-require_relative 'errors'
+require_relative 'gitlab_config'
require_relative 'gitlab_logger'
require_relative 'gitlab_access'
require_relative 'gitlab_lfs_authentication'
+require_relative 'httpunix'
require_relative 'http_helper'
-require_relative 'action'
-class GitlabNet
+class GitlabNet # rubocop:disable Metrics/ClassLength
include HTTPHelper
+ class ApiUnreachableError < StandardError; end
+ class NotFound < StandardError; end
+
CHECK_TIMEOUT = 5
- GL_PROTOCOL = 'ssh'.freeze
- API_INACCESSIBLE_ERROR = 'API is not accessible'.freeze
- def check_access(cmd, gl_repository, repo, actor, changes, protocol = GL_PROTOCOL, env: {})
+ def check_access(cmd, gl_repository, repo, who, changes, protocol, env: {})
changes = changes.join("\n") unless changes.is_a?(String)
params = {
@@ -26,27 +29,56 @@ class GitlabNet
env: env
}
- params[actor.identifier_key.to_sym] = actor.id
+ who_sym, _, who_v = self.class.parse_who(who)
+ params[who_sym] = who_v
- resp = post("#{internal_api_endpoint}/allowed", params)
+ url = "#{internal_api_endpoint}/allowed"
+ resp = post(url, params)
- determine_action(actor, resp)
+ if resp.code == '200'
+ GitAccessStatus.create_from_json(resp.body)
+ else
+ GitAccessStatus.new(false,
+ 'API is not accessible',
+ gl_repository: nil,
+ gl_id: nil,
+ gl_username: nil,
+ repository_path: nil,
+ gitaly: nil,
+ git_protocol: nil)
+ end
end
- def discover(actor)
- resp = get("#{internal_api_endpoint}/discover?#{actor.identifier_key}=#{actor.id}")
- JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
- rescue JSON::ParserError, ApiUnreachableError
- nil
+ def discover(who)
+ _, who_k, who_v = self.class.parse_who(who)
+
+ resp = get("#{internal_api_endpoint}/discover?#{who_k}=#{who_v}")
+
+ JSON.parse(resp.body) rescue nil
end
- def lfs_authenticate(actor, repo)
- params = { project: sanitize_path(repo) }
- params[actor.identifier_key.to_sym] = actor.id
+ def lfs_authenticate(gl_id, repo)
+ id_sym, _, id = self.class.parse_who(gl_id)
+
+ if id_sym == :key_id
+ params = {
+ project: sanitize_path(repo),
+ key_id: id
+ }
+ elsif id_sym == :user_id
+ params = {
+ project: sanitize_path(repo),
+ user_id: id
+ }
+ else
+ raise ArgumentError, "lfs_authenticate() got unsupported GL_ID='#{gl_id}'!"
+ end
resp = post("#{internal_api_endpoint}/lfs_authenticate", params)
- GitlabLfsAuthentication.build_from_json(resp.body) if resp.code == HTTP_SUCCESS
+ if resp.code == '200'
+ GitlabLfsAuthentication.build_from_json(resp.body)
+ end
end
def broadcast_message
@@ -61,7 +93,11 @@ class GitlabNet
url += "&gl_repository=#{URI.escape(gl_repository)}" if gl_repository
resp = get(url)
- resp.code == HTTP_SUCCESS ? JSON.parse(resp.body) : []
+ if resp.code == '200'
+ JSON.parse(resp.body)
+ else
+ []
+ end
rescue
[]
end
@@ -70,17 +106,19 @@ class GitlabNet
get("#{internal_api_endpoint}/check", options: { read_timeout: CHECK_TIMEOUT })
end
- def authorized_key(full_key)
- resp = get("#{internal_api_endpoint}/authorized_keys?key=#{URI.escape(full_key, '+/=')}")
- JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
+ def authorized_key(key)
+ resp = get("#{internal_api_endpoint}/authorized_keys?key=#{URI.escape(key, '+/=')}")
+ JSON.parse(resp.body) if resp.code == "200"
rescue
nil
end
- def two_factor_recovery_codes(actor)
- params = { actor.identifier_key.to_sym => actor.id }
- resp = post("#{internal_api_endpoint}/two_factor_recovery_codes", params)
- JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
+ def two_factor_recovery_codes(gl_id)
+ id_sym, _, id = self.class.parse_who(gl_id)
+
+ resp = post("#{internal_api_endpoint}/two_factor_recovery_codes", id_sym => id)
+
+ JSON.parse(resp.body) if resp.code == '200'
rescue
{}
end
@@ -89,50 +127,51 @@ class GitlabNet
params = { gl_repository: gl_repository, project: repo_path }
resp = post("#{internal_api_endpoint}/notify_post_receive", params)
- resp.code == HTTP_SUCCESS
+ resp.code == '200'
rescue
false
end
- def post_receive(gl_repository, actor, changes)
- params = { gl_repository: gl_repository, identifier: actor.identifier, changes: changes }
+ def post_receive(gl_repository, identifier, changes)
+ params = {
+ gl_repository: gl_repository,
+ identifier: identifier,
+ changes: changes
+ }
resp = post("#{internal_api_endpoint}/post_receive", params)
- raise NotFoundError if resp.code == HTTP_NOT_FOUND
- JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
+ raise NotFound if resp.code == '404'
+
+ JSON.parse(resp.body) if resp.code == '200'
end
def pre_receive(gl_repository)
resp = post("#{internal_api_endpoint}/pre_receive", gl_repository: gl_repository)
- raise NotFoundError if resp.code == HTTP_NOT_FOUND
- JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
- end
+ raise NotFound if resp.code == '404'
- private
-
- def sanitize_path(repo)
- repo.delete("'")
+ JSON.parse(resp.body) if resp.code == '200'
end
- def determine_action(actor, resp)
- json = JSON.parse(resp.body)
- message = json['message']
-
- case resp.code
- when HTTP_SUCCESS
- # TODO: This raise can be removed once internal API can respond with correct
- # HTTP status codes, instead of relying upon parsing the body and
- # accessing the 'status' key.
- raise AccessDeniedError, message unless json['status']
-
- Action::Gitaly.create_from_json(actor, json)
- when HTTP_UNAUTHORIZED, HTTP_NOT_FOUND
- raise AccessDeniedError, message
+ def self.parse_who(who)
+ if who.start_with?("key-")
+ value = who.gsub("key-", "")
+ raise ArgumentError, "who='#{who}' is invalid!" unless value =~ /\A[0-9]+\z/
+ [:key_id, 'key_id', value]
+ elsif who.start_with?("user-")
+ value = who.gsub("user-", "")
+ raise ArgumentError, "who='#{who}' is invalid!" unless value =~ /\A[0-9]+\z/
+ [:user_id, 'user_id', value]
+ elsif who.start_with?("username-")
+ [:username, 'username', who.gsub("username-", "")]
else
- raise UnknownError, "#{API_INACCESSIBLE_ERROR}: #{message}"
+ raise ArgumentError, "who='#{who}' is invalid!"
end
- rescue JSON::ParserError
- raise UnknownError, API_INACCESSIBLE_ERROR
+ end
+
+ protected
+
+ def sanitize_path(repo)
+ repo.delete("'")
end
end
diff --git a/lib/gitlab_post_receive.rb b/lib/gitlab_post_receive.rb
index 9248582..cb9931d 100644
--- a/lib/gitlab_post_receive.rb
+++ b/lib/gitlab_post_receive.rb
@@ -8,18 +8,20 @@ require 'securerandom'
class GitlabPostReceive
include NamesHelper
- def initialize(gl_repository, repo_path, gl_id, changes)
+ attr_reader :config, :gl_repository, :repo_path, :changes, :jid
+
+ def initialize(gl_repository, repo_path, actor, changes)
@config = GitlabConfig.new
@gl_repository = gl_repository
@repo_path = repo_path.strip
- @gl_id = gl_id
+ @actor = actor
@changes = changes
@jid = SecureRandom.hex(12)
end
def exec
response = GitlabMetrics.measure("post-receive") do
- api.post_receive(gl_repository, actor, changes)
+ api.post_receive(gl_repository, @actor, changes)
end
return false unless response
@@ -33,18 +35,12 @@ class GitlabPostReceive
false
end
- private
-
- attr_reader :config, :gl_repository, :repo_path, :gl_id, :changes, :jid
+ protected
def api
@api ||= GitlabNet.new
end
- def actor
- @actor ||= Actor.new_from(gl_id, audit_usernames: config.audit_usernames)
- end
-
def print_merge_request_links(merge_request_urls)
return if merge_request_urls.empty?
puts
@@ -104,6 +100,8 @@ class GitlabPostReceive
puts "=" * total_width
end
+ private
+
def parse_broadcast_msg(msg, text_length)
msg ||= ""
# just return msg if shorter than or equal to text length
diff --git a/lib/gitlab_shell.rb b/lib/gitlab_shell.rb
index bd7b783..78fdfe8 100644
--- a/lib/gitlab_shell.rb
+++ b/lib/gitlab_shell.rb
@@ -3,120 +3,299 @@ require 'pathname'
require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
-require_relative 'actor'
-class GitlabShell
- API_2FA_RECOVERY_CODES_COMMAND = '2fa_recovery_codes'.freeze
+class GitlabShell # rubocop:disable Metrics/ClassLength
+ class AccessDeniedError < StandardError; end
+ class DisallowedCommandError < StandardError; end
+ class InvalidRepositoryPathError < StandardError; end
- GIT_UPLOAD_PACK_COMMAND = 'git-upload-pack'.freeze
- GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack'.freeze
- GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive'.freeze
- GIT_LFS_AUTHENTICATE_COMMAND = 'git-lfs-authenticate'.freeze
+ GIT_COMMANDS = %w(git-upload-pack git-receive-pack git-upload-archive git-lfs-authenticate).freeze
+ GITALY_MIGRATED_COMMANDS = {
+ 'git-upload-pack' => File.join(ROOT_PATH, 'bin', 'gitaly-upload-pack'),
+ 'git-upload-archive' => File.join(ROOT_PATH, 'bin', 'gitaly-upload-archive'),
+ 'git-receive-pack' => File.join(ROOT_PATH, 'bin', 'gitaly-receive-pack')
+ }.freeze
+ API_COMMANDS = %w(2fa_recovery_codes).freeze
+ GL_PROTOCOL = 'ssh'.freeze
- GIT_COMMANDS = [GIT_UPLOAD_PACK_COMMAND, GIT_RECEIVE_PACK_COMMAND,
- GIT_UPLOAD_ARCHIVE_COMMAND, GIT_LFS_AUTHENTICATE_COMMAND].freeze
-
- Struct.new('ParsedCommand', :command, :git_access_command, :repo_name, :args)
+ attr_accessor :gl_id, :gl_repository, :repo_name, :command, :git_access, :git_protocol
+ attr_reader :repo_path
def initialize(who)
+ who_sym, = GitlabNet.parse_who(who)
+ if who_sym == :username
+ @who = who
+ else
+ @gl_id = who
+ end
@config = GitlabConfig.new
- @actor = Actor.new_from(who, audit_usernames: @config.audit_usernames)
end
# The origin_cmd variable contains UNTRUSTED input. If the user ran
# ssh git@gitlab.example.com 'evil command', then origin_cmd contains
# 'evil command'.
def exec(origin_cmd)
- if !origin_cmd || origin_cmd.empty?
- puts "Welcome to GitLab, #{actor.username}!"
+ unless origin_cmd
+ puts "Welcome to GitLab, #{username}!"
return true
end
- parsed_command = parse_cmd(origin_cmd)
- action = determine_action(parsed_command)
- action.execute(parsed_command.command, parsed_command.args)
+ args = Shellwords.shellwords(origin_cmd)
+ args = parse_cmd(args)
+
+ if GIT_COMMANDS.include?(args.first)
+ GitlabMetrics.measure('verify-access') { verify_access }
+ elsif !defined?(@gl_id)
+ # We're processing an API command like 2fa_recovery_codes, but
+ # don't have a @gl_id yet, that means we're in the "username"
+ # mode and need to materialize it, calling the "user" method
+ # will do that and call the /discover method.
+ user
+ end
+
+ process_cmd(args)
+
+ true
rescue GitlabNet::ApiUnreachableError
$stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
false
- rescue AccessDeniedError, UnknownError => ex
- $logger.warn('Access denied', command: origin_cmd, user: actor.log_username)
+ rescue AccessDeniedError => ex
+ $logger.warn('Access denied', command: origin_cmd, user: log_username)
+
$stderr.puts "GitLab: #{ex.message}"
false
rescue DisallowedCommandError
- $logger.warn('Denied disallowed command', command: origin_cmd, user: actor.log_username)
- $stderr.puts 'GitLab: Disallowed command'
+ $logger.warn('Denied disallowed command', command: origin_cmd, user: log_username)
+
+ $stderr.puts "GitLab: Disallowed command"
false
rescue InvalidRepositoryPathError
- $stderr.puts 'GitLab: Invalid repository path'
+ $stderr.puts "GitLab: Invalid repository path"
false
end
- private
-
- attr_reader :config, :actor
-
- def parse_cmd(cmd)
- args = Shellwords.shellwords(cmd)
+ protected
+ def parse_cmd(args)
# Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack
if args.length == 3 && args.first == 'git'
- command = "git-#{args[1]}"
- args = [command, args.last]
+ @command = "git-#{args[1]}"
+ args = [@command, args.last]
else
- command = args.first
+ @command = args.first
end
- git_access_command = command
+ @git_access = @command
- if command == API_2FA_RECOVERY_CODES_COMMAND
- return Struct::ParsedCommand.new(command, git_access_command, nil, args)
- end
+ return args if API_COMMANDS.include?(@command)
- raise DisallowedCommandError unless GIT_COMMANDS.include?(command)
+ raise DisallowedCommandError unless GIT_COMMANDS.include?(@command)
- case command
+ case @command
when 'git-lfs-authenticate'
raise DisallowedCommandError unless args.count >= 2
- repo_name = args[1]
- git_access_command = case args[2]
- when 'download'
- GIT_UPLOAD_PACK_COMMAND
- when 'upload'
- GIT_RECEIVE_PACK_COMMAND
- else
- raise DisallowedCommandError
- end
+ @repo_name = args[1]
+ case args[2]
+ when 'download'
+ @git_access = 'git-upload-pack'
+ when 'upload'
+ @git_access = 'git-receive-pack'
+ else
+ raise DisallowedCommandError
+ end
else
raise DisallowedCommandError unless args.count == 2
- repo_name = args.last
+ @repo_name = args.last
end
- Struct::ParsedCommand.new(command, git_access_command, repo_name, args)
+ args
end
- def determine_action(parsed_command)
- return Action::API2FARecovery.new(actor) if parsed_command.command == API_2FA_RECOVERY_CODES_COMMAND
+ def verify_access
+ status = api.check_access(@git_access, nil, @repo_name, @who || @gl_id, '_any', GL_PROTOCOL)
+
+ raise AccessDeniedError, status.message unless status.allowed?
+
+ self.repo_path = status.repository_path
+ @gl_repository = status.gl_repository
+ @git_protocol = ENV['GIT_PROTOCOL']
+ @gitaly = status.gitaly
+ @username = status.gl_username
+ if defined?(@who)
+ @gl_id = status.gl_id
+ end
+ end
- GitlabMetrics.measure('verify-access') do
- # GitlabNet#check_access will raise exception in the event of a problem
- initial_action = api.check_access(
- parsed_command.git_access_command,
- nil,
- parsed_command.repo_name,
- actor,
- '_any'
+ def process_cmd(args)
+ return send("api_#{@command}") if API_COMMANDS.include?(@command)
+
+ if @command == 'git-lfs-authenticate'
+ GitlabMetrics.measure('lfs-authenticate') do
+ $logger.info('Processing LFS authentication', user: log_username)
+ lfs_authenticate
+ end
+ return
+ end
+
+ executable = @command
+ args = [repo_path]
+
+ if GITALY_MIGRATED_COMMANDS.key?(executable) && @gitaly
+ executable = GITALY_MIGRATED_COMMANDS[executable]
+
+ gitaly_address = @gitaly['address']
+
+ # The entire gitaly_request hash should be built in gitlab-ce and passed
+ # on as-is. For now we build a fake one on the spot.
+ gitaly_request = {
+ 'repository' => @gitaly['repository'],
+ 'gl_repository' => @gl_repository,
+ 'gl_id' => @gl_id,
+ 'gl_username' => @username,
+ 'git_protocol' => @git_protocol
+ }
+
+ args = [gitaly_address, JSON.dump(gitaly_request)]
+ end
+
+ args_string = [File.basename(executable), *args].join(' ')
+ $logger.info('executing git command', command: args_string, user: log_username)
+ exec_cmd(executable, *args)
+ end
+
+ # This method is not covered by Rspec because it ends the current Ruby process.
+ def exec_cmd(*args)
+ # If you want to call a command without arguments, use
+ # exec_cmd(['my_command', 'my_command']) . Otherwise use
+ # exec_cmd('my_command', 'my_argument', ...).
+ if args.count == 1 && !args.first.is_a?(Array)
+ raise DisallowedCommandError
+ end
+
+ env = {
+ 'HOME' => ENV['HOME'],
+ 'PATH' => ENV['PATH'],
+ 'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
+ 'LANG' => ENV['LANG'],
+ 'GL_ID' => @gl_id,
+ 'GL_PROTOCOL' => GL_PROTOCOL,
+ 'GL_REPOSITORY' => @gl_repository,
+ 'GL_USERNAME' => @username
+ }
+ if @gitaly && @gitaly.include?('token')
+ env['GITALY_TOKEN'] = @gitaly['token']
+ end
+
+ if git_trace_available?
+ env.merge!(
+ 'GIT_TRACE' => @config.git_trace_log_file,
+ 'GIT_TRACE_PACKET' => @config.git_trace_log_file,
+ 'GIT_TRACE_PERFORMANCE' => @config.git_trace_log_file
)
+ end
- case parsed_command.command
- when GIT_LFS_AUTHENTICATE_COMMAND
- Action::GitLFSAuthenticate.new(actor, parsed_command.repo_name)
+ # We use 'chdir: ROOT_PATH' to let the next executable know where config.yml is.
+ Kernel.exec(env, *args, unsetenv_others: true, chdir: ROOT_PATH)
+ end
+
+ def api
+ GitlabNet.new
+ end
+
+ def user
+ return @user if defined?(@user)
+
+ begin
+ if defined?(@who)
+ @user = api.discover(@who)
+ @gl_id = "user-#{@user['id']}"
else
- initial_action
+ @user = api.discover(@gl_id)
end
+ rescue GitlabNet::ApiUnreachableError
+ @user = nil
end
end
- def api
- @api ||= GitlabNet.new
+ def username_from_discover
+ return nil unless user && user['username']
+
+ "@#{user['username']}"
+ end
+
+ def username
+ @username ||= username_from_discover || 'Anonymous'
+ end
+
+ # User identifier to be used in log messages.
+ def log_username
+ @config.audit_usernames ? username : "user with id #{@gl_id}"
+ end
+
+ def lfs_authenticate
+ lfs_access = api.lfs_authenticate(@gl_id, @repo_name)
+
+ return unless lfs_access
+
+ puts lfs_access.authentication_payload
+ end
+
+ private
+
+ def continue?(question)
+ puts "#{question} (yes/no)"
+ STDOUT.flush # Make sure the question gets output before we wait for input
+ continue = STDIN.gets.chomp
+ puts '' # Add a buffer in the output
+ continue == 'yes'
+ end
+
+ def api_2fa_recovery_codes
+ continue = continue?(
+ "Are you sure you want to generate new two-factor recovery codes?\n" \
+ "Any existing recovery codes you saved will be invalidated."
+ )
+
+ unless continue
+ puts 'New recovery codes have *not* been generated. Existing codes will remain valid.'
+ return
+ end
+
+ resp = api.two_factor_recovery_codes(@gl_id)
+ if resp['success']
+ codes = resp['recovery_codes'].join("\n")
+ puts "Your two-factor authentication recovery codes are:\n\n" \
+ "#{codes}\n\n" \
+ "During sign in, use one of the codes above when prompted for\n" \
+ "your two-factor code. Then, visit your Profile Settings and add\n" \
+ "a new device so you do not lose access to your account again."
+ else
+ puts "An error occurred while trying to generate new recovery codes.\n" \
+ "#{resp['message']}"
+ end
+ end
+
+ def git_trace_available?
+ return false unless @config.git_trace_log_file
+
+ if Pathname(@config.git_trace_log_file).relative?
+ $logger.warn('git trace log path must be absolute, ignoring', git_trace_log_file: @config.git_trace_log_file)
+ return false
+ end
+
+ begin
+ File.open(@config.git_trace_log_file, 'a') { nil }
+ return true
+ rescue => ex
+ $logger.warn('Failed to open git trace log file', git_trace_log_file: @config.git_trace_log_file, error: ex.to_s)
+ return false
+ end
+ end
+
+ def repo_path=(repo_path)
+ raise ArgumentError, "Repository path not provided. Please make sure you're using GitLab v8.10 or later." unless repo_path
+ raise InvalidRepositoryPathError if File.absolute_path(repo_path) != repo_path
+
+ @repo_path = repo_path
end
end
diff --git a/lib/http_helper.rb b/lib/http_helper.rb
index 9c7e564..62d0c51 100644
--- a/lib/http_helper.rb
+++ b/lib/http_helper.rb
@@ -1,20 +1,5 @@
-require 'net/http'
-require 'openssl'
-
-require_relative 'gitlab_config'
-require_relative 'httpunix'
-
module HTTPHelper
READ_TIMEOUT = 300
- HTTP_SUCCESS = '200'.freeze
- HTTP_MULTIPLE_CHOICES = '300'.freeze
- HTTP_UNAUTHORIZED = '401'.freeze
- HTTP_NOT_FOUND = '404'.freeze
-
- HTTP_SUCCESS_LIKE = [HTTP_SUCCESS, HTTP_MULTIPLE_CHOICES].freeze
-
- class ApiUnreachableError < StandardError; end
- class NotFoundError < StandardError; end
protected
@@ -91,7 +76,7 @@ module HTTPHelper
$logger.info('finished HTTP request', method: method.to_s.upcase, url: url, duration: Time.new - start_time)
end
- if HTTP_SUCCESS_LIKE.include?(response.code)
+ if response.code == "200"
$logger.debug('Received response', code: response.code, body: response.body)
else
$logger.error('Call failed', method: method.to_s.upcase, url: url, code: response.code, body: response.body)
@@ -124,7 +109,7 @@ module HTTPHelper
end
def secret_token
- @secret_token ||= File.read(config.secret_file)
+ @secret_token ||= File.read config.secret_file
end
def read_timeout