summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorStan Hu <stanhu@gmail.com>2018-09-07 22:30:53 +0000
committerStan Hu <stanhu@gmail.com>2018-09-07 22:30:53 +0000
commit2cf0298fa5d7c4da8bed44200359dd69a6a08eca (patch)
treec2cf728f776f60f8bae83a418d5a2c2faac66324 /lib
parent4f0a474cb88f7edae994e82d2ca977b1e6ed2e47 (diff)
parent524d523ed654469858a7a482d56f4d4661c61036 (diff)
downloadgitlab-shell-2cf0298fa5d7c4da8bed44200359dd69a6a08eca.tar.gz
Merge branch 'ash.mckenzie/custom-action-support' into 'master'v8.3.0
Custom Action support See merge request gitlab-org/gitlab-shell!215
Diffstat (limited to 'lib')
-rw-r--r--lib/action.rb4
-rw-r--r--lib/action/custom.rb129
-rw-r--r--lib/gitlab_access_status.rb21
-rw-r--r--lib/gitlab_net.rb17
-rw-r--r--lib/gitlab_shell.rb55
-rw-r--r--lib/http_codes.rb3
-rw-r--r--lib/http_helper.rb2
7 files changed, 197 insertions, 34 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..ba0e650
--- /dev/null
+++ b/lib/action/custom.rb
@@ -0,0 +1,129 @@
+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)
+ message = body['message']
+ message = Base64.decode64(body['result']) if !message && body['result'] && !body['result'].empty?
+ message ? message : 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 00ea8de..8483863 100644
--- a/lib/gitlab_access_status.rb
+++ b/lib/gitlab_access_status.rb
@@ -1,10 +1,16 @@
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, message, gl_repository:, gl_id:, gl_username:, gitaly:, git_protocol:, git_config_options:)
+ 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
@gl_repository = gl_repository
@gl_id = gl_id
@@ -12,21 +18,28 @@ class GitAccessStatus
@git_config_options = git_config_options
@gitaly = gitaly
@git_protocol = git_protocol
+ @payload = payload
end
- def self.create_from_json(json)
+ def self.create_from_json(json, status_code)
values = JSON.parse(json)
new(values["status"],
+ status_code,
values["message"],
gl_repository: values["gl_repository"],
gl_id: values["gl_id"],
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 5af2da6..57ae452 100644
--- a/lib/gitlab_net.rb
+++ b/lib/gitlab_net.rb
@@ -3,10 +3,8 @@ require 'openssl'
require 'json'
require_relative 'gitlab_config'
-require_relative 'gitlab_logger'
require_relative 'gitlab_access'
require_relative 'gitlab_lfs_authentication'
-require_relative 'httpunix'
require_relative 'http_helper'
class GitlabNet # rubocop:disable Metrics/ClassLength
@@ -35,18 +33,11 @@ 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,
- 'API is not accessible',
- gl_repository: nil,
- gl_id: nil,
- gl_username: nil,
- git_config_options: nil,
- gitaly: nil,
- git_protocol: nil)
+ GitAccessStatus.new(false, resp.code, 'API is not accessible')
end
end
diff --git a/lib/gitlab_shell.rb b/lib/gitlab_shell.rb
index 4d0b26f..79af861 100644
--- a/lib/gitlab_shell.rb
+++ b/lib/gitlab_shell.rb
@@ -5,16 +5,22 @@ require 'pathname'
require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
+require_relative 'action'
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'
+ GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack'
+ GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive'
+ GIT_LFS_AUTHENTICATE_COMMAND = 'git-lfs-authenticate'
+
GITALY_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')
+ GIT_UPLOAD_PACK_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-upload-pack'),
+ GIT_UPLOAD_ARCHIVE_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-upload-archive'),
+ GIT_RECEIVE_PACK_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-receive-pack')
}.freeze
GIT_COMMANDS = (GITALY_COMMANDS.keys + ['git-lfs-authenticate']).freeze
@@ -45,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"
@@ -55,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
@@ -63,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
@@ -94,14 +118,14 @@ class GitlabShell # rubocop:disable Metrics/ClassLength
raise DisallowedCommandError unless GIT_COMMANDS.include?(@command)
case @command
- when 'git-lfs-authenticate'
+ when GIT_LFS_AUTHENTICATE_COMMAND
raise DisallowedCommandError unless args.count >= 2
@repo_name = args[1]
case args[2]
when 'download'
- @git_access = 'git-upload-pack'
+ @git_access = GIT_UPLOAD_PACK_COMMAND
when 'upload'
- @git_access = 'git-receive-pack'
+ @git_access = GIT_RECEIVE_PACK_COMMAND
else
raise DisallowedCommandError
end
@@ -118,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)
diff --git a/lib/http_codes.rb b/lib/http_codes.rb
index 24b3b5a..5f79095 100644
--- a/lib/http_codes.rb
+++ b/lib/http_codes.rb
@@ -1,5 +1,8 @@
module HTTPCodes
HTTP_SUCCESS = '200'.freeze
+ HTTP_CREATED = '201'.freeze
+ HTTP_MULTIPLE_CHOICES = '300'.freeze
HTTP_UNAUTHORIZED = '401'.freeze
HTTP_NOT_FOUND = '404'.freeze
+ HTTP_SUCCESS_CODES = [HTTP_SUCCESS, HTTP_CREATED, HTTP_MULTIPLE_CHOICES].freeze
end
diff --git a/lib/http_helper.rb b/lib/http_helper.rb
index 1e75833..55c504c 100644
--- a/lib/http_helper.rb
+++ b/lib/http_helper.rb
@@ -1,4 +1,6 @@
require_relative 'http_codes'
+require_relative 'httpunix'
+require_relative 'gitlab_logger'
module HTTPHelper
include HTTPCodes