diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/action.rb | 4 | ||||
-rw-r--r-- | lib/action/custom.rb | 127 | ||||
-rw-r--r-- | lib/gitlab_access_status.rb | 17 | ||||
-rw-r--r-- | lib/gitlab_net.rb | 6 | ||||
-rw-r--r-- | lib/gitlab_shell.rb | 38 |
5 files changed, 175 insertions, 17 deletions
diff --git a/lib/action.rb b/lib/action.rb new file mode 100644 index 0000000..28c1c14 --- /dev/null +++ b/lib/action.rb @@ -0,0 +1,4 @@ +require_relative 'action/custom' + +module Action +end diff --git a/lib/action/custom.rb b/lib/action/custom.rb new file mode 100644 index 0000000..83ea5fc --- /dev/null +++ b/lib/action/custom.rb @@ -0,0 +1,127 @@ +require 'base64' + +require_relative '../http_helper' + +module Action + class Custom + include HTTPHelper + + class BaseError < StandardError; end + class MissingPayloadError < BaseError; end + class MissingAPIEndpointsError < BaseError; end + class MissingDataError < BaseError; end + class UnsuccessfulError < BaseError; end + + NO_MESSAGE_TEXT = 'No message'.freeze + DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze + + def initialize(gl_id, payload) + @gl_id = gl_id + @payload = payload + end + + def execute + validate! + result = process_api_endpoints + + if result && HTTP_SUCCESS_CODES.include?(result.code) + result + else + raise_unsuccessful!(result) + end + end + + private + + attr_reader :gl_id, :payload + + def process_api_endpoints + output = '' + resp = nil + + data_with_gl_id = data.merge('gl_id' => gl_id) + + api_endpoints.each do |endpoint| + url = "#{base_url}#{endpoint}" + json = { 'data' => data_with_gl_id, 'output' => output } + + resp = post(url, {}, headers: DEFAULT_HEADERS, options: { json: json }) + return resp unless HTTP_SUCCESS_CODES.include?(resp.code) + + begin + body = JSON.parse(resp.body) + rescue JSON::ParserError + raise UnsuccessfulError, 'Response was not valid JSON' + end + + print_flush(body['result']) + + # In the context of the git push sequence of events, it's necessary to read + # stdin in order to capture output to pass onto subsequent commands + output = read_stdin + end + + resp + end + + def base_url + config.gitlab_url + end + + def data + @data ||= payload['data'] + end + + def api_endpoints + data['api_endpoints'] + end + + def config + @config ||= GitlabConfig.new + end + + def api + @api ||= GitlabNet.new + end + + def read_stdin + Base64.encode64($stdin.read) + end + + def print_flush(str) + return false unless str + print(Base64.decode64(str)) + STDOUT.flush + end + + def validate! + validate_payload! + validate_data! + validate_api_endpoints! + end + + def validate_payload! + raise MissingPayloadError if !payload.is_a?(Hash) || payload.empty? + end + + def validate_data! + raise MissingDataError unless data.is_a?(Hash) + end + + def validate_api_endpoints! + raise MissingAPIEndpointsError if !api_endpoints.is_a?(Array) || + api_endpoints.empty? + end + + def raise_unsuccessful!(result) + message = begin + body = JSON.parse(result.body) + body['message'] || Base64.decode64(body['result']) || NO_MESSAGE_TEXT + rescue JSON::ParserError + NO_MESSAGE_TEXT + end + + raise UnsuccessfulError, "#{message} (#{result.code})" + end + end +end diff --git a/lib/gitlab_access_status.rb b/lib/gitlab_access_status.rb index 68fbba1..8483863 100644 --- a/lib/gitlab_access_status.rb +++ b/lib/gitlab_access_status.rb @@ -1,9 +1,14 @@ require 'json' +require_relative 'http_codes' class GitAccessStatus - attr_reader :message, :gl_repository, :gl_id, :gl_username, :gitaly, :git_protocol, :git_config_options + include HTTPCodes - def initialize(status, status_code, message, gl_repository: nil, gl_id: nil, gl_username: nil, gitaly: nil, git_protocol: nil, git_config_options: nil) + attr_reader :message, :gl_repository, :gl_id, :gl_username, :gitaly, :git_protocol, :git_config_options, :payload + + def initialize(status, status_code, message, gl_repository: nil, gl_id: nil, + gl_username: nil, gitaly: nil, git_protocol: nil, + git_config_options: nil, payload: nil) @status = status @status_code = status_code @message = message @@ -13,6 +18,7 @@ class GitAccessStatus @git_config_options = git_config_options @gitaly = gitaly @git_protocol = git_protocol + @payload = payload end def self.create_from_json(json, status_code) @@ -25,10 +31,15 @@ class GitAccessStatus gl_username: values["gl_username"], git_config_options: values["git_config_options"], gitaly: values["gitaly"], - git_protocol: values["git_protocol"]) + git_protocol: values["git_protocol"], + payload: values["payload"]) end def allowed? @status end + + def custom_action? + @status_code == HTTP_MULTIPLE_CHOICES + end end diff --git a/lib/gitlab_net.rb b/lib/gitlab_net.rb index 080898e..57ae452 100644 --- a/lib/gitlab_net.rb +++ b/lib/gitlab_net.rb @@ -33,9 +33,9 @@ class GitlabNet # rubocop:disable Metrics/ClassLength url = "#{internal_api_endpoint}/allowed" resp = post(url, params) - case resp.code.to_s - when HTTP_SUCCESS, HTTP_UNAUTHORIZED, HTTP_NOT_FOUND - GitAccessStatus.create_from_json(resp.body) + case resp.code + when HTTP_SUCCESS, HTTP_MULTIPLE_CHOICES, HTTP_UNAUTHORIZED, HTTP_NOT_FOUND + GitAccessStatus.create_from_json(resp.body, resp.code) else GitAccessStatus.new(false, resp.code, 'API is not accessible') end diff --git a/lib/gitlab_shell.rb b/lib/gitlab_shell.rb index ba5baf7..79af861 100644 --- a/lib/gitlab_shell.rb +++ b/lib/gitlab_shell.rb @@ -5,6 +5,7 @@ require 'pathname' require_relative 'gitlab_net' require_relative 'gitlab_metrics' +require_relative 'action' class GitlabShell # rubocop:disable Metrics/ClassLength class AccessDeniedError < StandardError; end @@ -50,8 +51,17 @@ class GitlabShell # rubocop:disable Metrics/ClassLength args = Shellwords.shellwords(origin_cmd) args = parse_cmd(args) + access_status = nil + if GIT_COMMANDS.include?(args.first) - GitlabMetrics.measure('verify-access') { verify_access } + access_status = GitlabMetrics.measure('verify-access') { verify_access } + + @gl_repository = access_status.gl_repository + @git_protocol = ENV['GIT_PROTOCOL'] + @gitaly = access_status.gitaly + @username = access_status.gl_username + @git_config_options = access_status.git_config_options + @gl_id = access_status.gl_id if defined?(@who) 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" @@ -60,6 +70,13 @@ class GitlabShell # rubocop:disable Metrics/ClassLength user end + if @command == GIT_RECEIVE_PACK_COMMAND && access_status.custom_action? + # If the response from /api/v4/allowed is a HTTP 300, we need to perform + # a Custom Action and therefore should return and not call process_cmd() + # + return process_custom_action(access_status) + end + process_cmd(args) true @@ -68,17 +85,19 @@ class GitlabShell # rubocop:disable Metrics/ClassLength false 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: log_username) - $stderr.puts "GitLab: Disallowed command" false rescue InvalidRepositoryPathError $stderr.puts "GitLab: Invalid repository path" false + rescue Action::Custom::BaseError => ex + $logger.warn('Custom action error', command: origin_cmd, user: log_username) + $stderr.puts "GitLab: #{ex.message}" + false end protected @@ -123,14 +142,11 @@ class GitlabShell # rubocop:disable Metrics/ClassLength raise AccessDeniedError, status.message unless status.allowed? - @gl_repository = status.gl_repository - @git_protocol = ENV['GIT_PROTOCOL'] - @gitaly = status.gitaly - @username = status.gl_username - @git_config_options = status.git_config_options - if defined?(@who) - @gl_id = status.gl_id - end + status + end + + def process_custom_action(access_status) + Action::Custom.new(@gl_id, access_status.payload).execute end def process_cmd(args) |