diff options
author | Robert Speicher <robert@gitlab.com> | 2018-08-08 17:38:38 +0000 |
---|---|---|
committer | Robert Speicher <robert@gitlab.com> | 2018-08-08 17:38:38 +0000 |
commit | 3aaf4751e09262c53544a1987f59b1308af9b6c1 (patch) | |
tree | 019332604ac4853db5d80bca95f229c95d1fc298 | |
parent | c6577e0d75f51b017f2f332838b97c3ca5b497c0 (diff) | |
parent | 014691e057537a803e22223ea072065cc91938a7 (diff) | |
download | gitlab-shell-3aaf4751e09262c53544a1987f59b1308af9b6c1.tar.gz |
Merge branch 'ash.mckenzie/srp-refactor' into 'master'
Refactor that focuses on SRP improvements
See merge request gitlab-org/gitlab-shell!214
66 files changed, 2077 insertions, 1164 deletions
@@ -1,13 +1,12 @@ source "http://rubygems.org" group :development, :test do - gem 'guard', '~> 1.5.0' - gem 'guard-rspec', '~> 2.1.0' - gem 'listen', '~> 0.5.0' - gem 'rspec', '~> 2.0' + gem 'guard-rspec', '~> 4.0' + gem 'listen', '~> 3.0.0' + gem 'rspec', '~> 3.0' gem 'rspec-its', '~> 1.0.0' gem 'rubocop', '0.49.1', require: false gem 'simplecov', '~> 0.9.0', require: false - gem 'vcr', '~> 2.4.0' + gem 'vcr', '~> 4.0' gem 'webmock', '~> 1.9.0' end diff --git a/Gemfile.lock b/Gemfile.lock index 88fc8c0..4ed9ad1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,18 +9,32 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.3) docile (1.1.5) - guard (1.5.4) - listen (>= 0.4.2) - lumberjack (>= 1.0.2) - pry (>= 0.9.10) - thor (>= 0.14.6) - guard-rspec (2.1.2) - guard (>= 1.1) - rspec (~> 2.11) - listen (0.5.3) + ffi (1.9.25) + formatador (0.2.5) + guard (2.14.2) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + lumberjack (>= 1.0.12, < 2.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-rspec (4.7.3) + guard (~> 2.1) + guard-compat (~> 1.1) + rspec (>= 2.99.0, < 4.0) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) lumberjack (1.0.13) method_source (0.9.0) multi_json (1.13.1) + nenv (0.3.0) + notiffany (0.1.1) + nenv (~> 0.1) + shellany (~> 0.0) parallel (1.12.1) parser (2.5.1.2) ast (~> 2.4.0) @@ -32,17 +46,25 @@ GEM rainbow (2.2.2) rake rake (12.3.1) - rspec (2.99.0) - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) - rspec-core (2.99.2) - rspec-expectations (2.99.2) - diff-lcs (>= 1.1.3, < 2.0) + rb-fsevent (0.10.3) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) rspec-its (1.0.1) rspec-core (>= 2.99.0.beta1) rspec-expectations (>= 2.99.0.beta1) - rspec-mocks (2.99.4) + rspec-mocks (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) rubocop (0.49.1) parallel (~> 1.10) parser (>= 2.3.3.1, < 3.0) @@ -52,6 +74,7 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.9.0) safe_yaml (1.0.4) + shellany (0.0.1) simplecov (0.9.2) docile (~> 1.1.0) multi_json (~> 1.0) @@ -59,7 +82,7 @@ GEM simplecov-html (0.9.0) thor (0.20.0) unicode-display_width (1.4.0) - vcr (2.4.0) + vcr (4.0.0) webmock (1.9.3) addressable (>= 2.2.7) crack (>= 0.3.2) @@ -68,14 +91,13 @@ PLATFORMS ruby DEPENDENCIES - guard (~> 1.5.0) - guard-rspec (~> 2.1.0) - listen (~> 0.5.0) - rspec (~> 2.0) + guard-rspec (~> 4.0) + listen (~> 3.0.0) + rspec (~> 3.0) rspec-its (~> 1.0.0) rubocop (= 0.49.1) simplecov (~> 0.9.0) - vcr (~> 2.4.0) + vcr (~> 4.0) webmock (~> 1.9.0) BUNDLED WITH @@ -1,23 +1,7 @@ -# A sample Guardfile # More info at https://github.com/guard/guard#readme -guard 'rspec' do +guard :rspec, cmd: 'bundle exec rspec' do watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { "spec" } - - # Rails example - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch('config/routes.rb') { "spec/routing" } - watch('app/controllers/application_controller.rb') { "spec/controllers" } - - # Capybara features specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" } - - # Turnip features and steps - watch(%r{^spec/acceptance/(.+)\.feature$}) - watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } end diff --git a/bin/authorized_keys b/bin/authorized_keys index ca01646..24484f1 100755 --- a/bin/authorized_keys +++ b/bin/authorized_keys @@ -10,16 +10,16 @@ # command="/bin/gitlab-shell key-#",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQA... # -key = ARGV[0] -abort "# No key provided" if key.nil? || key.empty? +full_key = ARGV[0] +abort "# No key provided" if full_key.nil? || full_key.empty? require_relative "../lib/gitlab_init" require_relative "../lib/gitlab_net" require_relative "../lib/gitlab_keys" -authorized_key = GitlabNet.new.authorized_key(key) +authorized_key = GitlabNet.new.authorized_key(full_key) if authorized_key.nil? - puts "# No key was found for #{key}" + puts "# No key was found for #{full_key}" else - puts GitlabKeys.key_line("key-#{authorized_key['id']}", authorized_key["key"]) + puts GitlabKeys.key_line("key-#{authorized_key['id']}", authorized_key['key']) end diff --git a/bin/gitlab-keys b/bin/gitlab-keys index 9eb1950..c0284e8 100755 --- a/bin/gitlab-keys +++ b/bin/gitlab-keys @@ -13,7 +13,7 @@ require_relative '../lib/gitlab_init' # /bin/gitlab-keys rm-key key-23 "ssh-rsa AAAAx321..." # # /bin/gitlab-keys list-keys -# +# # /bin/gitlab-keys clear # diff --git a/bin/gitlab-shell-authorized-keys-check b/bin/gitlab-shell-authorized-keys-check index 2ea1a74..b2cd24c 100755 --- a/bin/gitlab-shell-authorized-keys-check +++ b/bin/gitlab-shell-authorized-keys-check @@ -5,7 +5,7 @@ # command for a given ssh key fingerprint # # Ex. -# bin/gitlab-shell-authorized-keys-check <username> <public-key> +# bin/gitlab-shell-authorized-keys-check <expected-username> <actual-username> <public-key> # # Returns # command="/bin/gitlab-shell key-#",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAA... @@ -27,16 +27,16 @@ abort '# No username provided' if actual_username.nil? || actual_username == '' # Normally, these would both be 'git', but it can be configured by the user exit 0 unless expected_username == actual_username -key = ARGV[2] -abort "# No key provided" if key.nil? || key == '' +full_key = ARGV[2] +abort "# No key provided" if full_key.nil? || full_key == '' require_relative '../lib/gitlab_init' require_relative '../lib/gitlab_net' require_relative '../lib/gitlab_keys' -authorized_key = GitlabNet.new.authorized_key(key) +authorized_key = GitlabNet.new.authorized_key(full_key) if authorized_key.nil? - puts "# No key was found for #{key}" + puts "# No key was found for #{full_key}" else puts GitlabKeys.key_line("key-#{authorized_key['id']}", authorized_key['key']) end diff --git a/bin/gitlab-shell-authorized-principals-check b/bin/gitlab-shell-authorized-principals-check index aa6d427..4b39cac 100755 --- a/bin/gitlab-shell-authorized-principals-check +++ b/bin/gitlab-shell-authorized-principals-check @@ -6,7 +6,7 @@ # the right options. # # Ex. -# bin/gitlab-shell-authorized-keys-check <key-id> <principal1> [<principal2>...] +# bin/gitlab-shell-authorized-principals-check <key-id> <principal1> [<principal2>...] # # Returns one line per principal passed in, e.g.: # command="/bin/gitlab-shell username-{KEY_ID}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty {PRINCIPAL} @@ -23,9 +23,9 @@ key_id = ARGV[0] abort '# No key_id provided' if key_id.nil? || key_id == '' principals = ARGV[1..-1] -principals.each { |principal| - abort '# An invalid principal was provided' if principal.nil? || principal == '' -} +principals.each do |principal| + abort '# An invalid principal was provided' if principal.nil? || principal == '' +end require_relative '../lib/gitlab_init' require_relative '../lib/gitlab_net' diff --git a/hooks/post-receive b/hooks/post-receive index 30f4be1..33d9f8d 100755 --- a/hooks/post-receive +++ b/hooks/post-receive @@ -4,15 +4,15 @@ # will be processed properly. refs = $stdin.read -key_id = ENV.delete('GL_ID') +gl_id = ENV.delete('GL_ID') gl_repository = ENV['GL_REPOSITORY'] repo_path = Dir.pwd require_relative '../lib/gitlab_custom_hook' require_relative '../lib/gitlab_post_receive' -if GitlabPostReceive.new(gl_repository, repo_path, key_id, refs).exec && - GitlabCustomHook.new(repo_path, key_id).post_receive(refs) +if GitlabPostReceive.new(gl_repository, repo_path, gl_id, refs).exec && + GitlabCustomHook.new(repo_path, gl_id).post_receive(refs) exit 0 else exit 1 diff --git a/hooks/pre-receive b/hooks/pre-receive index 6ce5879..66c61d9 100755 --- a/hooks/pre-receive +++ b/hooks/pre-receive @@ -4,7 +4,7 @@ # will be processed properly. refs = $stdin.read -key_id = ENV.delete('GL_ID') +gl_id = ENV.delete('GL_ID') protocol = ENV.delete('GL_PROTOCOL') repo_path = Dir.pwd gl_repository = ENV['GL_REPOSITORY'] @@ -23,8 +23,8 @@ require_relative '../lib/gitlab_net' # last so that it only runs if everything else succeeded. On post-receive on the # other hand, we run GitlabPostReceive first because the push is already done # and we don't want to skip it if the custom hook fails. -if GitlabAccess.new(gl_repository, repo_path, key_id, refs, protocol).exec && - GitlabCustomHook.new(repo_path, key_id).pre_receive(refs) && +if GitlabAccess.new(gl_repository, repo_path, gl_id, refs, protocol).exec && + GitlabCustomHook.new(repo_path, gl_id).pre_receive(refs) && increase_reference_counter(gl_repository, repo_path) exit 0 else diff --git a/hooks/update b/hooks/update index 4c2fc08..263a3e8 100755 --- a/hooks/update +++ b/hooks/update @@ -7,11 +7,11 @@ ref_name = ARGV[0] old_value = ARGV[1] new_value = ARGV[2] repo_path = Dir.pwd -key_id = ENV.delete('GL_ID') +gl_id = ENV.delete('GL_ID') require_relative '../lib/gitlab_custom_hook' -if GitlabCustomHook.new(repo_path, key_id).update(ref_name, old_value, new_value) +if GitlabCustomHook.new(repo_path, gl_id).update(ref_name, old_value, new_value) exit 0 else exit 1 diff --git a/lib/action.rb b/lib/action.rb new file mode 100644 index 0000000..1f9cc6c --- /dev/null +++ b/lib/action.rb @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..06f8057 --- /dev/null +++ b/lib/action/api_2fa_recovery.rb @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000..fe8c836 --- /dev/null +++ b/lib/action/base.rb @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..8c5294d --- /dev/null +++ b/lib/action/git_lfs_authenticate.rb @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..b95ff17 --- /dev/null +++ b/lib/action/gitaly.rb @@ -0,0 +1,126 @@ +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 new file mode 100644 index 0000000..4e8b3b8 --- /dev/null +++ b/lib/actor.rb @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000..0510c60 --- /dev/null +++ b/lib/actor/base.rb @@ -0,0 +1,56 @@ +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 new file mode 100644 index 0000000..46f013a --- /dev/null +++ b/lib/actor/key.rb @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..55ba7f1 --- /dev/null +++ b/lib/actor/user.rb @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..cd9d6e5 --- /dev/null +++ b/lib/actor/username.rb @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..7bef3b4 --- /dev/null +++ b/lib/errors.rb @@ -0,0 +1,4 @@ +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 e1a5e35..6683ee7 100644 --- a/lib/gitlab_access.rb +++ b/lib/gitlab_access.rb @@ -1,34 +1,28 @@ +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 - attr_reader :config, :gl_repository, :repo_path, :changes, :protocol - - def initialize(gl_repository, repo_path, actor, changes, protocol) - @config = GitlabConfig.new + def initialize(gl_repository, repo_path, gl_id, changes, protocol) @gl_repository = gl_repository @repo_path = repo_path.strip - @actor = actor + @gl_id = gl_id @changes = changes.lines @protocol = protocol end def exec - 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) + 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" @@ -38,9 +32,19 @@ class GitlabAccess false end - protected + private + + attr_reader :gl_repository, :repo_path, :gl_id, :changes, :protocol def api - GitlabNet.new + @api ||= GitlabNet.new + end + + def config + @config ||= GitlabConfig.new + end + + def actor + @actor ||= Actor.new_from(gl_id, audit_usernames: config.audit_usernames) end end diff --git a/lib/gitlab_access_status.rb b/lib/gitlab_access_status.rb deleted file mode 100644 index c639462..0000000 --- a/lib/gitlab_access_status.rb +++ /dev/null @@ -1,32 +0,0 @@ -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 67096df..de6eb38 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, key_id) + def initialize(repo_path, gl_id) @repo_path = repo_path - @vars = { 'GL_ID' => key_id } + @vars = { 'GL_ID' => gl_id } @config = GitlabConfig.new end diff --git a/lib/gitlab_keys.rb b/lib/gitlab_keys.rb index 3ee2882..d0463e2 100644 --- a/lib/gitlab_keys.rb +++ b/lib/gitlab_keys.rb @@ -9,6 +9,7 @@ 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 @@ -21,6 +22,7 @@ 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 9cb7e56..9ea18aa 100644 --- a/lib/gitlab_net.rb +++ b/lib/gitlab_net.rb @@ -1,23 +1,20 @@ -require 'net/http' -require 'openssl' require 'json' -require_relative 'gitlab_config' +require_relative 'errors' 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 # rubocop:disable Metrics/ClassLength +class GitlabNet 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, who, changes, protocol, env: {}) + def check_access(cmd, gl_repository, repo, actor, changes, protocol = GL_PROTOCOL, env: {}) changes = changes.join("\n") unless changes.is_a?(String) params = { @@ -29,56 +26,27 @@ class GitlabNet # rubocop:disable Metrics/ClassLength env: env } - who_sym, _, who_v = self.class.parse_who(who) - params[who_sym] = who_v + params[actor.identifier_key.to_sym] = actor.id - url = "#{internal_api_endpoint}/allowed" - resp = post(url, params) + resp = post("#{internal_api_endpoint}/allowed", params) - 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 + determine_action(actor, resp) end - 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 + 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 end - 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 + def lfs_authenticate(actor, repo) + params = { project: sanitize_path(repo) } + params[actor.identifier_key.to_sym] = actor.id resp = post("#{internal_api_endpoint}/lfs_authenticate", params) - if resp.code == '200' - GitlabLfsAuthentication.build_from_json(resp.body) - end + GitlabLfsAuthentication.build_from_json(resp.body) if resp.code == HTTP_SUCCESS end def broadcast_message @@ -93,11 +61,7 @@ class GitlabNet # rubocop:disable Metrics/ClassLength url += "&gl_repository=#{URI.escape(gl_repository)}" if gl_repository resp = get(url) - if resp.code == '200' - JSON.parse(resp.body) - else - [] - end + resp.code == HTTP_SUCCESS ? JSON.parse(resp.body) : [] rescue [] end @@ -106,19 +70,17 @@ class GitlabNet # rubocop:disable Metrics/ClassLength get("#{internal_api_endpoint}/check", options: { read_timeout: CHECK_TIMEOUT }) end - def authorized_key(key) - resp = get("#{internal_api_endpoint}/authorized_keys?key=#{URI.escape(key, '+/=')}") - JSON.parse(resp.body) if resp.code == "200" + 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 rescue nil end - 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' + 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 rescue {} end @@ -127,51 +89,50 @@ class GitlabNet # rubocop:disable Metrics/ClassLength params = { gl_repository: gl_repository, project: repo_path } resp = post("#{internal_api_endpoint}/notify_post_receive", params) - resp.code == '200' + resp.code == HTTP_SUCCESS rescue false end - def post_receive(gl_repository, identifier, changes) - params = { - gl_repository: gl_repository, - identifier: identifier, - changes: changes - } + def post_receive(gl_repository, actor, changes) + params = { gl_repository: gl_repository, identifier: actor.identifier, changes: changes } resp = post("#{internal_api_endpoint}/post_receive", params) + raise NotFoundError if resp.code == HTTP_NOT_FOUND - raise NotFound if resp.code == '404' - - JSON.parse(resp.body) if resp.code == '200' + JSON.parse(resp.body) if resp.code == HTTP_SUCCESS 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 - raise NotFound if resp.code == '404' - - JSON.parse(resp.body) if resp.code == '200' - end - - 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 ArgumentError, "who='#{who}' is invalid!" - end + JSON.parse(resp.body) if resp.code == HTTP_SUCCESS end - protected + private def sanitize_path(repo) repo.delete("'") 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 + else + raise UnknownError, "#{API_INACCESSIBLE_ERROR}: #{message}" + end + rescue JSON::ParserError + raise UnknownError, API_INACCESSIBLE_ERROR + end end diff --git a/lib/gitlab_post_receive.rb b/lib/gitlab_post_receive.rb index cb9931d..9248582 100644 --- a/lib/gitlab_post_receive.rb +++ b/lib/gitlab_post_receive.rb @@ -8,20 +8,18 @@ require 'securerandom' class GitlabPostReceive include NamesHelper - attr_reader :config, :gl_repository, :repo_path, :changes, :jid - - def initialize(gl_repository, repo_path, actor, changes) + def initialize(gl_repository, repo_path, gl_id, changes) @config = GitlabConfig.new @gl_repository = gl_repository @repo_path = repo_path.strip - @actor = actor + @gl_id = gl_id @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 @@ -35,12 +33,18 @@ class GitlabPostReceive false end - protected + private + + attr_reader :config, :gl_repository, :repo_path, :gl_id, :changes, :jid 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 @@ -100,8 +104,6 @@ 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 78fdfe8..bd7b783 100644 --- a/lib/gitlab_shell.rb +++ b/lib/gitlab_shell.rb @@ -3,299 +3,120 @@ require 'pathname' require_relative 'gitlab_net' require_relative 'gitlab_metrics' +require_relative 'actor' -class GitlabShell # rubocop:disable Metrics/ClassLength - class AccessDeniedError < StandardError; end - class DisallowedCommandError < StandardError; end - class InvalidRepositoryPathError < StandardError; end +class GitlabShell + API_2FA_RECOVERY_CODES_COMMAND = '2fa_recovery_codes'.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_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 - attr_accessor :gl_id, :gl_repository, :repo_name, :command, :git_access, :git_protocol - attr_reader :repo_path + 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) 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) - unless origin_cmd - puts "Welcome to GitLab, #{username}!" + if !origin_cmd || origin_cmd.empty? + puts "Welcome to GitLab, #{actor.username}!" return true end - 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 + parsed_command = parse_cmd(origin_cmd) + action = determine_action(parsed_command) + action.execute(parsed_command.command, parsed_command.args) rescue GitlabNet::ApiUnreachableError $stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable" false - rescue AccessDeniedError => ex - $logger.warn('Access denied', command: origin_cmd, user: log_username) - + rescue AccessDeniedError, UnknownError => ex + $logger.warn('Access denied', command: origin_cmd, user: actor.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" + $logger.warn('Denied disallowed command', command: origin_cmd, user: actor.log_username) + $stderr.puts 'GitLab: Disallowed command' false rescue InvalidRepositoryPathError - $stderr.puts "GitLab: Invalid repository path" + $stderr.puts 'GitLab: Invalid repository path' false end - protected + private + + attr_reader :config, :actor + + def parse_cmd(cmd) + args = Shellwords.shellwords(cmd) - 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 + git_access_command = command - return args if API_COMMANDS.include?(@command) + if command == API_2FA_RECOVERY_CODES_COMMAND + return Struct::ParsedCommand.new(command, git_access_command, nil, args) + end - 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] - case args[2] - when 'download' - @git_access = 'git-upload-pack' - when 'upload' - @git_access = 'git-receive-pack' - else - raise DisallowedCommandError - end + 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 else raise DisallowedCommandError unless args.count == 2 - @repo_name = args.last + repo_name = args.last end - args + Struct::ParsedCommand.new(command, git_access_command, repo_name, args) end - 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 + def determine_action(parsed_command) + return Action::API2FARecovery.new(actor) if parsed_command.command == API_2FA_RECOVERY_CODES_COMMAND - 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 + 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' ) - 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 api - GitlabNet.new - end - - def user - return @user if defined?(@user) - - begin - if defined?(@who) - @user = api.discover(@who) - @gl_id = "user-#{@user['id']}" + case parsed_command.command + when GIT_LFS_AUTHENTICATE_COMMAND + Action::GitLFSAuthenticate.new(actor, parsed_command.repo_name) else - @user = api.discover(@gl_id) + initial_action end - rescue GitlabNet::ApiUnreachableError - @user = nil - end - end - - 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 + def api + @api ||= GitlabNet.new end end diff --git a/lib/http_helper.rb b/lib/http_helper.rb index 62d0c51..9c7e564 100644 --- a/lib/http_helper.rb +++ b/lib/http_helper.rb @@ -1,5 +1,20 @@ +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 @@ -76,7 +91,7 @@ module HTTPHelper $logger.info('finished HTTP request', method: method.to_s.upcase, url: url, duration: Time.new - start_time) end - if response.code == "200" + if HTTP_SUCCESS_LIKE.include?(response.code) $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) @@ -109,7 +124,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 diff --git a/spec/action/api_2fa_recovery.rb_spec.rb b/spec/action/api_2fa_recovery.rb_spec.rb new file mode 100644 index 0000000..70091e9 --- /dev/null +++ b/spec/action/api_2fa_recovery.rb_spec.rb @@ -0,0 +1,73 @@ +require_relative '../spec_helper' +require_relative '../../lib/action/api_2fa_recovery' + +describe Action::API2FARecovery do + let(:key_id) { '1' } + let(:actor) { Actor::Key.new(key_id) } + let(:username) { 'testuser' } + let(:discover_payload) { { 'username' => username } } + let(:api) { double(GitlabNet) } + + before do + allow(GitlabNet).to receive(:new).and_return(api) + allow(api).to receive(:discover).with(actor).and_return(discover_payload) + end + + subject do + described_class.new(actor) + end + + describe '#execute' do + context 'with an invalid repsonse' do + it 'returns nil' do + expect($stdin).to receive(:gets).and_return("meh\n") + + expect do + expect(subject.execute(nil, nil)).to be_nil + end.to output(/New recovery codes have \*not\* been generated/).to_stdout + end + end + + context 'with a negative response' do + before do + expect(subject).to receive(:continue?).and_return(false) + end + + it 'returns nil' do + expect do + expect(subject.execute(nil, nil)).to be_nil + end.to output(/New recovery codes have \*not\* been generated/).to_stdout + end + end + + + context 'with an affirmative response' do + let(:recovery_codes) { %w{ 8dfe0f433208f40b289904c6072e4a72 c33cee7fd0a73edb56e61b785e49af03 } } + + before do + expect(subject).to receive(:continue?).and_return(true) + expect(api).to receive(:two_factor_recovery_codes).with(actor).and_return(response) + end + + context 'with a unsuccessful response' do + let(:response) { { 'success' => false } } + + it 'puts error message to stdout' do + expect do + expect(subject.execute(nil, nil)).to be_falsey + end.to output(/An error occurred while trying to generate new recovery codes/).to_stdout + end + end + + context 'with a successful response' do + let(:response) { { 'success' => true, 'recovery_codes' => recovery_codes } } + + it 'puts information message including recovery codes to stdout' do + expect do + expect(subject.execute(nil, nil)).to be_truthy + end.to output(Regexp.new(recovery_codes.join("\n"))).to_stdout + end + end + end + end +end diff --git a/spec/action/base_spec.rb b/spec/action/base_spec.rb new file mode 100644 index 0000000..e986378 --- /dev/null +++ b/spec/action/base_spec.rb @@ -0,0 +1,12 @@ +require_relative '../spec_helper' +require_relative '../../lib/action/base' + +describe Action::Base do + describe '.create_from_json' do + it 'raises a NotImplementedError exeption' do + expect do + described_class.create_from_json('nomatter') + end.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/action/git_lfs_authenticate_spec.rb b/spec/action/git_lfs_authenticate_spec.rb new file mode 100644 index 0000000..07e844f --- /dev/null +++ b/spec/action/git_lfs_authenticate_spec.rb @@ -0,0 +1,48 @@ +require_relative '../spec_helper' +require_relative '../../lib/action/git_lfs_authenticate' + +describe Action::GitLFSAuthenticate do + let(:key_id) { '1' } + let(:repo_name) { 'gitlab-ci.git' } + let(:actor) { Actor::Key.new(key_id) } + let(:username) { 'testuser' } + let(:discover_payload) { { 'username' => username } } + let(:api) { double(GitlabNet) } + + before do + allow(GitlabNet).to receive(:new).and_return(api) + allow(api).to receive(:discover).with(actor).and_return(discover_payload) + end + + subject do + described_class.new(actor, repo_name) + end + + describe '#execute' do + context 'when response from API is not a success' do + before do + expect(api).to receive(:lfs_authenticate).with(actor, repo_name).and_return(nil) + end + + it 'returns nil' do + expect(subject.execute(nil, nil)).to be_nil + end + end + + context 'when response from API is a success' do + let(:username) { 'testuser' } + let(:lfs_token) { '1234' } + let(:repository_http_path) { "/tmp/#{repo_name}" } + let(:gitlab_lfs_authentication) { GitlabLfsAuthentication.new(username, lfs_token, repository_http_path) } + + before do + expect(api).to receive(:lfs_authenticate).with(actor, repo_name).and_return(gitlab_lfs_authentication) + end + + it 'puts payload to stdout' do + expect($stdout).to receive(:puts).with('{"header":{"Authorization":"Basic dGVzdHVzZXI6MTIzNA=="},"href":"/tmp/gitlab-ci.git/info/lfs/"}') + expect(subject.execute(nil, nil)).to be_truthy + end + end + end +end diff --git a/spec/action/gitaly_spec.rb b/spec/action/gitaly_spec.rb new file mode 100644 index 0000000..a5f6f0b --- /dev/null +++ b/spec/action/gitaly_spec.rb @@ -0,0 +1,137 @@ +require_relative '../spec_helper' +require_relative '../../lib/action/gitaly' + +describe Action::Gitaly do + let(:git_trace_log_file_valid) { '/tmp/git_trace_performance.log' } + let(:git_trace_log_file_invalid) { "/bleep-bop#{git_trace_log_file_valid}" } + let(:git_trace_log_file_relative) { "..#{git_trace_log_file_valid}" } + let(:key_id) { '1' } + let(:key_str) { 'key-1' } + let(:key) { Actor::Key.new(key_id) } + let(:gl_repository) { 'project-1' } + let(:gl_username) { 'testuser' } + let(:git_protocol) { 'version=2' } + let(:tmp_repos_path) { File.join(ROOT_PATH, 'tmp', 'repositories') } + let(:repo_name) { 'gitlab-ci.git' } + let(:repository_path) { File.join(tmp_repos_path, repo_name) } + let(:gitaly_address) { 'unix:gitaly.socket' } + let(:gitaly_token) { '123456' } + let(:gitaly) do + { + 'repository' => { 'relative_path' => repo_name, 'storage_name' => 'default' }, + 'address' => gitaly_address, + 'token' => gitaly_token + } + end + + describe '.create_from_json' do + it 'returns an instance of Action::Gitaly' do + json = { + "gl_repository" => gl_repository, + "gl_username" => gl_username, + "repository_path" => repository_path, + "gitaly" => gitaly + } + expect(described_class.create_from_json(key_id, json)).to be_instance_of(Action::Gitaly) + end + end + + subject do + described_class.new(key, gl_repository, gl_username, git_protocol, repository_path, gitaly) + end + + describe '#execute' do + let(:args) { [ repository_path ] } + let(:base_exec_env) do + { + 'HOME' => ENV['HOME'], + 'PATH' => ENV['PATH'], + 'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'], + 'LANG' => ENV['LANG'], + 'GL_ID' => key_str, + 'GL_PROTOCOL' => GitlabNet::GL_PROTOCOL, + 'GL_REPOSITORY' => gl_repository, + 'GL_USERNAME' => gl_username, + 'GITALY_TOKEN' => gitaly_token, + } + end + let(:with_trace_exec_env) do + base_exec_env.merge({ + 'GIT_TRACE' => git_trace_log_file, + 'GIT_TRACE_PACKET' => git_trace_log_file, + 'GIT_TRACE_PERFORMANCE' => git_trace_log_file + }) + end + let(:gitaly_request) do + { + 'repository' => gitaly['repository'], + 'gl_repository' => gl_repository, + 'gl_id' => key_str, + 'gl_username' => gl_username, + 'git_protocol' => git_protocol + } + end + + context 'for migrated commands' do + context 'such as git-upload-pack' do + let(:git_trace_log_file) { nil } + let(:command) { 'git-upload-pack' } + + before do + allow_any_instance_of(GitlabConfig).to receive(:git_trace_log_file).and_return(git_trace_log_file) + end + + context 'with an invalid config.git_trace_log_file' do + let(:git_trace_log_file) { git_trace_log_file_invalid } + + it 'returns true' do + expect(Kernel).to receive(:exec).with( + base_exec_env, + described_class::MIGRATED_COMMANDS[command], + gitaly_address, + JSON.dump(gitaly_request), + unsetenv_others: true, + chdir: ROOT_PATH + ).and_return(true) + + expect(subject.execute(command, args)).to be_truthy + end + end + + context 'with n relative config.git_trace_log_file' do + let(:git_trace_log_file) { git_trace_log_file_relative } + + it 'returns true' do + expect(Kernel).to receive(:exec).with( + base_exec_env, + described_class::MIGRATED_COMMANDS[command], + gitaly_address, + JSON.dump(gitaly_request), + unsetenv_others: true, + chdir: ROOT_PATH + ).and_return(true) + + expect(subject.execute(command, args)).to be_truthy + end + end + + context 'with a valid config.git_trace_log_file' do + let(:git_trace_log_file) { git_trace_log_file_valid } + + it 'returns true' do + expect(Kernel).to receive(:exec).with( + with_trace_exec_env, + described_class::MIGRATED_COMMANDS[command], + gitaly_address, + JSON.dump(gitaly_request), + unsetenv_others: true, + chdir: ROOT_PATH + ).and_return(true) + + expect(subject.execute(command, args)).to be_truthy + end + end + end + end + end +end diff --git a/spec/actor/base_spec.rb b/spec/actor/base_spec.rb new file mode 100644 index 0000000..26f899d --- /dev/null +++ b/spec/actor/base_spec.rb @@ -0,0 +1,28 @@ +require_relative '../spec_helper' +require_relative '../../lib/actor/base' + +describe Actor::Base do + describe '.identifier_key' do + it 'raises a NotImplementedError exception' do + expect do + described_class.identifier_key + end.to raise_error(NotImplementedError) + end + end + + describe '.identifier_prefix' do + it 'raises a NotImplementedError exception' do + expect do + described_class.identifier_prefix + end.to raise_error(NotImplementedError) + end + end + + describe '.id_regex' do + it 'raises a NotImplementedError exception' do + expect do + described_class.id_regex + end.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/actor/key_spec.rb b/spec/actor/key_spec.rb new file mode 100644 index 0000000..e61a083 --- /dev/null +++ b/spec/actor/key_spec.rb @@ -0,0 +1,80 @@ +require_relative '../spec_helper' +require_relative '../../lib/actor/key' + +describe Actor::Key do + let(:key_id) { '1' } + let(:username) { 'testuser' } + let(:api) { double(GitlabNet) } + + let(:discover_payload) { { 'username' => username } } + let(:audit_usernames) { nil } + + before do + allow(GitlabNet).to receive(:new).and_return(api) + allow(api).to receive(:discover).with(subject).and_return(discover_payload) + end + + describe '.from' do + it 'returns an instance of Actor::Key' do + expect(described_class.from('key-1')).to be_a(Actor::Key) + end + + it 'has a key_id == 1' do + expect(described_class.from('key-1').key_id).to eq '1' + end + end + + describe '.identifier_prefix' do + it "returns 'key'" do + expect(described_class.identifier_prefix).to eql 'key' + end + end + + describe '.identifier_key' do + it "returns 'key_id'" do + expect(described_class.identifier_key).to eql 'key_id' + end + end + + subject { described_class.new(key_id, audit_usernames: audit_usernames) } + + describe '#username' do + context 'with a valid user' do + it "returns '@testuser'" do + expect(subject.username).to eql '@testuser' + end + end + + context 'without a valid user' do + let(:discover_payload) { nil } + + it "returns 'Anonymous'" do + expect(subject.username).to eql 'Anonymous' + end + end + end + + describe '#identifier' do + it "returns 'key-1'" do + expect(subject.identifier).to eql 'key-1' + end + end + + describe '#log_username' do + context 'when audit_usernames is true' do + let(:audit_usernames) { true } + + it "returns 'testuser'" do + expect(subject.log_username).to eql '@testuser' + end + end + + context 'when audit_usernames is false' do + let(:audit_usernames) { false } + + it "returns 'key with identifier key-1'" do + expect(subject.log_username).to eql 'key with identifier key-1' + end + end + end +end diff --git a/spec/actor/user_spec.rb b/spec/actor/user_spec.rb new file mode 100644 index 0000000..311fe73 --- /dev/null +++ b/spec/actor/user_spec.rb @@ -0,0 +1,62 @@ +require_relative '../spec_helper' +require_relative '../../lib/actor/user' + +describe Actor::User do + let(:user_id) { '1' } + let(:username) { 'testuser' } + let(:audit_usernames) { nil } + + describe '.from' do + it 'returns an instance of Actor::User' do + expect(described_class.from('user-1')).to be_a(Actor::User) + end + + it 'has an id == 1' do + expect(described_class.from('user-1').id).to eq '1' + end + end + + describe '.identifier_prefix' do + it "returns 'user'" do + expect(described_class.identifier_prefix).to eql 'user' + end + end + + describe '.identifier_key' do + it "returns 'user_id'" do + expect(described_class.identifier_key).to eql 'user_id' + end + end + + subject { described_class.new(user_id, audit_usernames: audit_usernames) } + + describe '#username' do + it "returns 'user-1'" do + expect(subject.username).to eql 'user-1' + end + end + + describe '#identifier' do + it "returns 'user-1'" do + expect(subject.identifier).to eql 'user-1' + end + end + + describe '#log_username' do + context 'when audit_usernames is true' do + let(:audit_usernames) { true } + + it "returns 'user-1'" do + expect(subject.log_username).to eql 'user-1' + end + end + + context 'when audit_usernames is false' do + let(:audit_usernames) { false } + + it "returns 'user with identifier user-1'" do + expect(subject.log_username).to eql 'user with identifier user-1' + end + end + end +end diff --git a/spec/actor/username_spec.rb b/spec/actor/username_spec.rb new file mode 100644 index 0000000..a02bf12 --- /dev/null +++ b/spec/actor/username_spec.rb @@ -0,0 +1,79 @@ +require_relative '../spec_helper' +require_relative '../../lib/actor/username' + +describe Actor::Username do + let(:username) { 'testuser' } + let(:api) { double(GitlabNet) } + + let(:discover_payload) { { 'username' => username } } + let(:audit_usernames) { nil } + + before do + allow(GitlabNet).to receive(:new).and_return(api) + allow(api).to receive(:discover).with(subject).and_return(discover_payload) + end + + describe '.from' do + it 'returns an instance of Actor::Username' do + expect(described_class.from("username-#{username}")).to be_a(Actor::Username) + end + + it 'has an id == 1' do + expect(described_class.from('username-1').id).to eq '1' + end + end + + describe '.identifier_prefix' do + it "returns 'user'" do + expect(described_class.identifier_prefix).to eql 'username' + end + end + + describe '.identifier_key' do + it "returns 'username'" do + expect(described_class.identifier_key).to eql 'username' + end + end + + subject { described_class.new(username, audit_usernames: audit_usernames) } + + describe '#username' do + context 'without a valid user' do + it "returns '@testuser'" do + expect(subject.username).to eql "@#{username}" + end + end + + context 'without a valid user' do + let(:discover_payload) { nil } + + it "returns 'Anonymous'" do + expect(subject.username).to eql 'Anonymous' + end + end + end + + describe '#identifier' do + it "returns 'username-testuser'" do + expect(subject.identifier).to eql 'username-testuser' + end + end + + describe '#log_username' do + context 'when audit_usernames is true' do + let(:audit_usernames) { true } + + it "returns '@testuser'" do + expect(subject.log_username).to eql "@#{username}" + end + end + + context 'when audit_usernames is false' do + let(:audit_usernames) { false } + + it "returns 'user with identifier username-testuser'" do + expect(subject.log_username).to eql "user with identifier username-#{username}" + end + end + end +end diff --git a/spec/actor_spec.rb b/spec/actor_spec.rb new file mode 100644 index 0000000..57cc718 --- /dev/null +++ b/spec/actor_spec.rb @@ -0,0 +1,34 @@ +require_relative 'spec_helper' +require_relative '../lib/actor' + +describe Actor do + describe '.new_from' do + context 'for an unsupported Actor type' do + it 'raises a NotImplementedError exception' do + expect do + described_class.new_from('unsupported-1') + end.to raise_error(Actor::UnsupportedActorError) + end + end + + context 'for a supported Actor type' do + context 'of Key' do + it 'returns an instance of Key' do + expect(described_class.new_from('key-1')).to be_a(Actor::Key) + end + end + + context 'of User' do + it 'returns an instance of User' do + expect(described_class.new_from('user-1')).to be_a(Actor::User) + end + end + + context 'of Username' do + it 'returns an instance of Username' do + expect(described_class.new_from('username-john1')).to be_a(Actor::Username) + end + end + end + end +end diff --git a/spec/gitlab_access_spec.rb b/spec/gitlab_access_spec.rb index f9717a7..73ebf2e 100644 --- a/spec/gitlab_access_spec.rb +++ b/spec/gitlab_access_spec.rb @@ -7,31 +7,27 @@ describe GitlabAccess do let(:repo_path) { File.join(repository_path, repo_name) + ".git" } let(:api) do double(GitlabNet).tap do |api| - api.stub(check_access: GitAccessStatus.new(true, - 'ok', - gl_repository: 'project-1', - gl_id: 'user-123', - gl_username: 'testuser', - repository_path: '/home/git/repositories', - gitaly: nil, - git_protocol: 'version=2')) + allow(api).to receive(:check_access).and_return( + Action::Gitaly.new( + 'key-1', + 'project-1', + 'testuser', + 'version=2', + '/home/git/repositories', + nil + ) + ) end end subject do GitlabAccess.new(nil, repo_path, 'key-123', 'wow', 'ssh').tap do |access| - access.stub(exec_cmd: :exec_called) - access.stub(api: api) + allow(access).to receive(:exec_cmd).and_return(:exec_called) + allow(access).to receive(:api).and_return(api) end end before do - GitlabConfig.any_instance.stub(repos_path: repository_path) - end - - describe :initialize do - it { subject.repo_path.should == repo_path } - it { subject.changes.should == ['wow'] } - it { subject.protocol.should == 'ssh' } + allow_any_instance_of(GitlabConfig).to receive(:repos_path).and_return(repository_path) end describe "#exec" do @@ -43,16 +39,7 @@ describe GitlabAccess do context "access is denied" do before do - api.stub(check_access: GitAccessStatus.new( - false, - 'denied', - gl_repository: nil, - gl_id: nil, - gl_username: nil, - repository_path: nil, - gitaly: nil, - git_protocol: nil - )) + allow(api).to receive(:check_access).and_raise(AccessDeniedError) end it "returns false" do @@ -62,7 +49,7 @@ describe GitlabAccess do context "API connection fails" do before do - api.stub(:check_access).and_raise(GitlabNet::ApiUnreachableError) + allow(api).to receive(:check_access).and_raise(GitlabNet::ApiUnreachableError) end it "returns false" do diff --git a/spec/gitlab_config_spec.rb b/spec/gitlab_config_spec.rb index a1c52a0..dd7e3bc 100644 --- a/spec/gitlab_config_spec.rb +++ b/spec/gitlab_config_spec.rb @@ -3,29 +3,55 @@ require_relative '../lib/gitlab_config' describe GitlabConfig do let(:config) { GitlabConfig.new } + let(:config_data) do + { + # 'user' => 'git', + # 'http_settings' => { + # 'self_signed_cert' => false + # }, + # 'log_level' => 'ERROR', + # 'audit_usernames' => true, + # 'log_format' => 'json', # Not sure on other values? + # 'git_trace_log_file' => nil + } + end + + before do + expect(YAML).to receive(:load_file).and_return(config_data) + end - describe :gitlab_url do + describe '#gitlab_url' do let(:url) { 'http://test.com' } + subject { config.gitlab_url } - before { config.send(:config)['gitlab_url'] = url } + + before { config_data['gitlab_url'] = url } it { should_not be_empty } it { should eq(url) } context 'remove trailing slashes' do - before { config.send(:config)['gitlab_url'] = url + '//' } + before { config_data['gitlab_url'] = url + '//' } it { should eq(url) } end end - describe :audit_usernames do + describe '#audit_usernames' do subject { config.audit_usernames } it("returns false by default") { should eq(false) } end - describe :log_format do + describe '#log_level' do + subject { config.log_level } + + it 'returns "INFO" by default' do + should eq('INFO') + end + end + + describe '#log_format' do subject { config.log_format } it 'returns "text" by default' do diff --git a/spec/gitlab_keys_spec.rb b/spec/gitlab_keys_spec.rb index 7011ca0..3cd6798 100644 --- a/spec/gitlab_keys_spec.rb +++ b/spec/gitlab_keys_spec.rb @@ -65,9 +65,9 @@ describe GitlabKeys do describe :initialize do let(:gitlab_keys) { build_gitlab_keys('add-key', 'key-741', 'ssh-rsa AAAAB3NzaDAxx2E') } - it { gitlab_keys.key.should == 'ssh-rsa AAAAB3NzaDAxx2E' } - it { gitlab_keys.instance_variable_get(:@command).should == 'add-key' } - it { gitlab_keys.instance_variable_get(:@key_id).should == 'key-741' } + it { expect(gitlab_keys.key).to eql 'ssh-rsa AAAAB3NzaDAxx2E' } + it { expect(gitlab_keys.instance_variable_get(:@command)).to eql 'add-key' } + it { expect(gitlab_keys.instance_variable_get(:@key_id)).to eql 'key-741' } end describe :add_key do @@ -77,7 +77,7 @@ describe GitlabKeys do create_authorized_keys_fixture gitlab_keys.send :add_key auth_line = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-741\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E" - File.read(tmp_authorized_keys_path).should == "existing content\n#{auth_line}\n" + expect(File.read(tmp_authorized_keys_path)).to eql "existing content\n#{auth_line}\n" end context "without file writing" do @@ -85,12 +85,12 @@ describe GitlabKeys do before { create_authorized_keys_fixture } it "should log an add-key event" do - $logger.should_receive(:info).with("Adding key", {:key_id=>"key-741", :public_key=>"ssh-rsa AAAAB3NzaDAxx2E"}) + expect($logger).to receive(:info).with("Adding key", {:key_id=>"key-741", :public_key=>"ssh-rsa AAAAB3NzaDAxx2E"}) gitlab_keys.send :add_key end it "should return true" do - gitlab_keys.send(:add_key).should be_truthy + expect(gitlab_keys.send(:add_key)).to be_truthy end end end @@ -104,7 +104,7 @@ describe GitlabKeys do create_authorized_keys_fixture gitlab_keys.send :add_key auth_line1 = 'key-741 AAAAB3NzaDAxx2E' - gitlab_keys.send(:list_keys).should == "#{auth_line1}\n" + expect(gitlab_keys.send(:list_keys)).to eql "#{auth_line1}\n" end end @@ -118,10 +118,9 @@ describe GitlabKeys do end it 'outputs the key IDs, separated by newlines' do - output = capture_stdout do + expect do gitlab_keys.send(:list_key_ids) - end - output.should match "1\n2\n3\n9000" + end.to output("1\n2\n3\n9000\n").to_stdout end end @@ -130,38 +129,38 @@ describe GitlabKeys do let(:fake_stdin) { StringIO.new("key-12\tssh-dsa ASDFASGADG\nkey-123\tssh-rsa GFDGDFSGSDFG\n", 'r') } before do create_authorized_keys_fixture - gitlab_keys.stub(stdin: fake_stdin) + allow(gitlab_keys).to receive(:stdin).and_return(fake_stdin) end it "adds lines at the end of the file" do gitlab_keys.send :batch_add_keys auth_line1 = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-12\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-dsa ASDFASGADG" auth_line2 = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-123\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa GFDGDFSGSDFG" - File.read(tmp_authorized_keys_path).should == "existing content\n#{auth_line1}\n#{auth_line2}\n" + expect(File.read(tmp_authorized_keys_path)).to eql "existing content\n#{auth_line1}\n#{auth_line2}\n" end context "with invalid input" do let(:fake_stdin) { StringIO.new("key-12\tssh-dsa ASDFASGADG\nkey-123\tssh-rsa GFDGDFSGSDFG\nfoo\tbar\tbaz\n", 'r') } it "aborts" do - gitlab_keys.should_receive(:abort) + expect(gitlab_keys).to receive(:abort) gitlab_keys.send :batch_add_keys end end context "without file writing" do before do - gitlab_keys.should_receive(:open).and_yield(double(:file, puts: nil, chmod: nil)) + expect(gitlab_keys).to receive(:open).and_yield(double(:file, puts: nil, chmod: nil)) end it "should log an add-key event" do - $logger.should_receive(:info).with("Adding key", key_id: 'key-12', public_key: "ssh-dsa ASDFASGADG") - $logger.should_receive(:info).with("Adding key", key_id: 'key-123', public_key: "ssh-rsa GFDGDFSGSDFG") + expect($logger).to receive(:info).with("Adding key", key_id: 'key-12', public_key: "ssh-dsa ASDFASGADG") + expect($logger).to receive(:info).with("Adding key", key_id: 'key-123', public_key: "ssh-rsa GFDGDFSGSDFG") gitlab_keys.send :batch_add_keys end it "should return true" do - gitlab_keys.send(:batch_add_keys).should be_truthy + expect(gitlab_keys.send(:batch_add_keys)).to be_truthy end end end @@ -187,22 +186,22 @@ describe GitlabKeys do end gitlab_keys.send :rm_key erased_line = delete_line.gsub(/./, '#') - File.read(tmp_authorized_keys_path).should == "existing content\n#{erased_line}\n#{other_line}\n" + expect(File.read(tmp_authorized_keys_path)).to eql "existing content\n#{erased_line}\n#{other_line}\n" end context "without file writing" do before do - gitlab_keys.stub(:open) - gitlab_keys.stub(:lock).and_yield + allow(gitlab_keys).to receive(:open) + allow(gitlab_keys).to receive(:lock).and_yield end it "should log an rm-key event" do - $logger.should_receive(:info).with("Removing key", key_id: "key-741") + expect($logger).to receive(:info).with("Removing key", key_id: "key-741") gitlab_keys.send :rm_key end it "should return true" do - gitlab_keys.send(:rm_key).should be_truthy + expect(gitlab_keys.send(:rm_key)).to be_truthy end end @@ -219,7 +218,7 @@ describe GitlabKeys do end gitlab_keys.send :rm_key erased_line = delete_line.gsub(/./, '#') - File.read(tmp_authorized_keys_path).should == "existing content\n#{erased_line}\n#{other_line}\n" + expect(File.read(tmp_authorized_keys_path)).to eql "existing content\n#{erased_line}\n#{other_line}\n" end end end @@ -228,8 +227,8 @@ describe GitlabKeys do let(:gitlab_keys) { build_gitlab_keys('clear') } it "should return true" do - gitlab_keys.stub(:open) - gitlab_keys.send(:clear).should be_truthy + allow(gitlab_keys).to receive(:open) + expect(gitlab_keys.send(:clear)).to be_truthy end end @@ -242,7 +241,7 @@ describe GitlabKeys do end it 'returns false if opening raises an exception' do - gitlab_keys.should_receive(:open_auth_file).and_raise("imaginary error") + expect(gitlab_keys).to receive(:open_auth_file).and_raise("imaginary error") expect(gitlab_keys.exec).to eq(false) end @@ -257,51 +256,51 @@ describe GitlabKeys do describe :exec do it 'add-key arg should execute add_key method' do gitlab_keys = build_gitlab_keys('add-key') - gitlab_keys.should_receive(:add_key) + expect(gitlab_keys).to receive(:add_key) gitlab_keys.exec end it 'batch-add-keys arg should execute batch_add_keys method' do gitlab_keys = build_gitlab_keys('batch-add-keys') - gitlab_keys.should_receive(:batch_add_keys) + expect(gitlab_keys).to receive(:batch_add_keys) gitlab_keys.exec end it 'rm-key arg should execute rm_key method' do gitlab_keys = build_gitlab_keys('rm-key') - gitlab_keys.should_receive(:rm_key) + expect(gitlab_keys).to receive(:rm_key) gitlab_keys.exec end it 'clear arg should execute clear method' do gitlab_keys = build_gitlab_keys('clear') - gitlab_keys.should_receive(:clear) + expect(gitlab_keys).to receive(:clear) gitlab_keys.exec end it 'check-permissions arg should execute check_permissions method' do gitlab_keys = build_gitlab_keys('check-permissions') - gitlab_keys.should_receive(:check_permissions) + expect(gitlab_keys).to receive(:check_permissions) gitlab_keys.exec end it 'should puts message if unknown command arg' do gitlab_keys = build_gitlab_keys('change-key') - gitlab_keys.should_receive(:puts).with('not allowed') + expect(gitlab_keys).to receive(:puts).with('not allowed') gitlab_keys.exec end it 'should log a warning on unknown commands' do gitlab_keys = build_gitlab_keys('nooope') - gitlab_keys.stub(puts: nil) - $logger.should_receive(:warn).with("Attempt to execute invalid gitlab-keys command", command: '"nooope"') + allow(gitlab_keys).to receive(:puts).and_return(nil) + expect($logger).to receive(:warn).with("Attempt to execute invalid gitlab-keys command", command: '"nooope"') gitlab_keys.exec end end describe :lock do before do - GitlabKeys.any_instance.stub(lock_file: tmp_lock_file_path) + allow_any_instance_of(GitlabKeys).to receive(:lock_file).and_return(tmp_lock_file_path) end it "should raise exception if operation lasts more then timeout" do @@ -310,7 +309,7 @@ describe GitlabKeys do key.send :lock, 1 do sleep 2 end - end.to raise_error + end.to raise_error(Timeout::Error) end it "should actually lock file" do @@ -335,7 +334,7 @@ describe GitlabKeys do end thr1.join - $global.should == "foobar" + expect($global).to eql "foobar" end end @@ -353,7 +352,7 @@ describe GitlabKeys do def create_authorized_keys_fixture(existing_content: 'existing content') FileUtils.mkdir_p(File.dirname(tmp_authorized_keys_path)) open(tmp_authorized_keys_path, 'w') { |file| file.puts(existing_content) } - gitlab_keys.stub(auth_file: tmp_authorized_keys_path) + allow(gitlab_keys).to receive(:auth_file).and_return(tmp_authorized_keys_path) end def tmp_authorized_keys_path diff --git a/spec/gitlab_lfs_authentication_spec.rb b/spec/gitlab_lfs_authentication_spec.rb index 9e93a07..5516bb5 100644 --- a/spec/gitlab_lfs_authentication_spec.rb +++ b/spec/gitlab_lfs_authentication_spec.rb @@ -16,22 +16,22 @@ describe GitlabLfsAuthentication do end describe '#build_from_json' do - it { subject.username.should == 'dzaporozhets' } - it { subject.lfs_token.should == 'wsnys8Zm8Jn7zyhHTAAK' } - it { subject.repository_http_path.should == 'http://gitlab.dev/repo' } + it { expect(subject.username).to eql 'dzaporozhets' } + it { expect(subject.lfs_token).to eql 'wsnys8Zm8Jn7zyhHTAAK' } + it { expect(subject.repository_http_path).to eql 'http://gitlab.dev/repo' } end describe '#authentication_payload' do result = "{\"header\":{\"Authorization\":\"Basic ZHphcG9yb3poZXRzOndzbnlzOFptOEpuN3p5aEhUQUFL\"},\"href\":\"http://gitlab.dev/repo/info/lfs/\"}" - it { subject.authentication_payload.should eq(result) } + it { expect(subject.authentication_payload).to eq(result) } it 'should be a proper JSON' do payload = subject.authentication_payload json_payload = JSON.parse(payload) - json_payload['header']['Authorization'].should eq('Basic ZHphcG9yb3poZXRzOndzbnlzOFptOEpuN3p5aEhUQUFL') - json_payload['href'].should eq('http://gitlab.dev/repo/info/lfs/') + expect(json_payload['header']['Authorization']).to eq('Basic ZHphcG9yb3poZXRzOndzbnlzOFptOEpuN3p5aEhUQUFL') + expect(json_payload['href']).to eq('http://gitlab.dev/repo/info/lfs/') end end end diff --git a/spec/gitlab_logger_spec.rb b/spec/gitlab_logger_spec.rb index 934b6fa..31c6cff 100644 --- a/spec/gitlab_logger_spec.rb +++ b/spec/gitlab_logger_spec.rb @@ -6,7 +6,7 @@ describe :convert_log_level do subject { convert_log_level :extreme } it "converts invalid log level to Logger::INFO" do - $stderr.should_receive(:puts).at_least(:once) + expect($stderr).to receive(:puts).at_least(:once) should eq(Logger::INFO) end end diff --git a/spec/gitlab_net_spec.rb b/spec/gitlab_net_spec.rb index be2f4ba..8df6fd3 100644 --- a/spec/gitlab_net_spec.rb +++ b/spec/gitlab_net_spec.rb @@ -1,6 +1,5 @@ require_relative 'spec_helper' require_relative '../lib/gitlab_net' -require_relative '../lib/gitlab_access_status' describe GitlabNet, vcr: true do let(:gitlab_net) { described_class.new } @@ -8,57 +7,66 @@ describe GitlabNet, vcr: true do let(:base_api_endpoint) { 'http://localhost:3000/api/v4' } let(:internal_api_endpoint) { 'http://localhost:3000/api/v4/internal' } let(:project) { 'gitlab-org/gitlab-test.git' } - let(:key) { 'key-1' } - let(:key2) { 'key-2' } + + let(:actor1) { Actor.new_from('key-1') } + let(:bad_actor1) { Actor.new_from('key-777') } + let(:actor2) { Actor.new_from('user-1') } + let(:secret) { "0a3938d9d95d807e94d937af3a4fbbea\n" } before do $logger = double('logger').as_null_object - gitlab_net.stub(:base_api_endpoint).and_return(base_api_endpoint) - gitlab_net.stub(:secret_token).and_return(secret) + allow(gitlab_net).to receive(:base_api_endpoint).and_return(base_api_endpoint) + allow(gitlab_net).to receive(:secret_token).and_return(secret) end describe '#check' do it 'should return 200 code for gitlab check' do VCR.use_cassette("check-ok") do result = gitlab_net.check - result.code.should == '200' + expect(result.code).to eql('200') end end it 'adds the secret_token to request' do VCR.use_cassette("check-ok") do - Net::HTTP::Get.any_instance.should_receive(:set_form_data).with(hash_including(secret_token: secret)) + allow_any_instance_of(Net::HTTP::Get).to receive(:set_form_data).with(hash_including(secret_token: secret)) gitlab_net.check end end it "raises an exception if the connection fails" do - Net::HTTP.any_instance.stub(:request).and_raise(StandardError) - expect { gitlab_net.check }.to raise_error(GitlabNet::ApiUnreachableError) + allow_any_instance_of(Net::HTTP).to receive(:request).and_raise(StandardError) + expect do gitlab_net.check end.to raise_error(GitlabNet::ApiUnreachableError) end end describe '#discover' do - it 'should return user has based on key id' do + it 'returns user has based on key id' do VCR.use_cassette("discover-ok") do - user = gitlab_net.discover(key) - user['name'].should == 'Administrator' - user['username'].should == 'root' + user = gitlab_net.discover(actor1) + expect(user['name']).to eql 'Administrator' + expect(user['username']).to eql 'root' + end + end + + it 'returns nil if the user cannot be found' do + VCR.use_cassette("discover-not-found") do + expect(gitlab_net.discover(actor1)).to be_nil end end it 'adds the secret_token to request' do VCR.use_cassette("discover-ok") do - Net::HTTP::Get.any_instance.should_receive(:set_form_data).with(hash_including(secret_token: secret)) - gitlab_net.discover(key) + allow_any_instance_of(Net::HTTP::Get).to receive(:set_form_data).with(hash_including(secret_token: secret)) + gitlab_net.discover(actor1) end end it "raises an exception if the connection fails" do VCR.use_cassette("discover-ok") do - Net::HTTP.any_instance.stub(:request).and_raise(StandardError) - expect { gitlab_net.discover(key) }.to raise_error(GitlabNet::ApiUnreachableError) + allow_any_instance_of(Net::HTTP).to receive(:request).and_raise(StandardError) + expect(gitlab_net.discover(actor1)).to be_nil end end end @@ -67,10 +75,10 @@ describe GitlabNet, vcr: true do context 'lfs authentication succeeded' do it 'should return the correct data' do VCR.use_cassette('lfs-authenticate-ok') do - lfs_access = gitlab_net.lfs_authenticate(key, project) - lfs_access.username.should == 'root' - lfs_access.lfs_token.should == 'Hyzhyde_wLUeyUQsR3tHGTG8eNocVQm4ssioTEsBSdb6KwCSzQ' - lfs_access.repository_http_path.should == URI.join(internal_api_endpoint.sub('api/v4', ''), project).to_s + lfs_access = gitlab_net.lfs_authenticate(actor1, project) + expect(lfs_access.username).to eql 'root' + expect(lfs_access.lfs_token).to eql 'Hyzhyde_wLUeyUQsR3tHGTG8eNocVQm4ssioTEsBSdb6KwCSzQ' + expect(lfs_access.repository_http_path).to eql URI.join(internal_api_endpoint.sub('api/v4', ''), project).to_s end end end @@ -81,7 +89,7 @@ describe GitlabNet, vcr: true do it 'should return message' do VCR.use_cassette("broadcast_message-ok") do result = gitlab_net.broadcast_message - result["message"].should == "Message" + expect(result["message"]).to eql "Message" end end end @@ -90,7 +98,7 @@ describe GitlabNet, vcr: true do it 'should return nil' do VCR.use_cassette("broadcast_message-none") do result = gitlab_net.broadcast_message - result.should == {} + expect(result).to eql({}) end end end @@ -102,13 +110,13 @@ describe GitlabNet, vcr: true do let(:encoded_changes) { "123456%20789012%20refs/heads/test%0A654321%20210987%20refs/tags/tag" } it "sends the given arguments as encoded URL parameters" do - gitlab_net.should_receive(:get).with("#{internal_api_endpoint}/merge_request_urls?project=#{project}&changes=#{encoded_changes}&gl_repository=#{gl_repository}") + expect(gitlab_net).to receive(:get).with("#{internal_api_endpoint}/merge_request_urls?project=#{project}&changes=#{encoded_changes}&gl_repository=#{gl_repository}") gitlab_net.merge_request_urls(gl_repository, project, changes) end it "omits the gl_repository parameter if it's nil" do - gitlab_net.should_receive(:get).with("#{internal_api_endpoint}/merge_request_urls?project=#{project}&changes=#{encoded_changes}") + expect(gitlab_net).to receive(:get).with("#{internal_api_endpoint}/merge_request_urls?project=#{project}&changes=#{encoded_changes}") gitlab_net.merge_request_urls(nil, project, changes) end @@ -135,8 +143,7 @@ describe GitlabNet, vcr: true do subject { gitlab_net.pre_receive(gl_repository) } it 'sends the correct parameters and returns the request body parsed' do - Net::HTTP::Post.any_instance.should_receive(:set_form_data) - .with(hash_including(params)) + allow_any_instance_of(Net::HTTP::Post).to receive(:set_form_data).with(hash_including(params)) VCR.use_cassette("pre-receive") { subject } end @@ -147,9 +154,9 @@ describe GitlabNet, vcr: true do end end - it 'throws a NotFound error when pre-receive is not available' do + it 'throws a NotFoundError when pre-receive is not available' do VCR.use_cassette("pre-receive-not-found") do - expect { subject }.to raise_error(GitlabNet::NotFound) + expect do subject end.to raise_error(GitlabNet::NotFoundError) end end end @@ -158,7 +165,7 @@ describe GitlabNet, vcr: true do let(:gl_repository) { "project-1" } let(:changes) { "123456 789012 refs/heads/test\n654321 210987 refs/tags/tag" } let(:params) do - { gl_repository: gl_repository, identifier: key, changes: changes } + { gl_repository: gl_repository, identifier: actor1.identifier, changes: changes } end let(:merge_request_urls) do [{ @@ -168,11 +175,10 @@ describe GitlabNet, vcr: true do }] end - subject { gitlab_net.post_receive(gl_repository, key, changes) } + subject { gitlab_net.post_receive(gl_repository, actor1, changes) } it 'sends the correct parameters' do - Net::HTTP::Post.any_instance.should_receive(:set_form_data).with(hash_including(params)) - + allow_any_instance_of(Net::HTTP::Post).to receive(:set_form_data).with(hash_including(params)) VCR.use_cassette("post-receive") do subject @@ -187,9 +193,9 @@ describe GitlabNet, vcr: true do end end - it 'throws a NotFound error when post-receive is not available' do + it 'throws a NotFoundError when post-receive is not available' do VCR.use_cassette("post-receive-not-found") do - expect { subject }.to raise_error(GitlabNet::NotFound) + expect do subject end.to raise_error(GitlabNet::NotFoundError) end end end @@ -200,21 +206,21 @@ describe GitlabNet, vcr: true do it "should return nil when the resource is not implemented" do VCR.use_cassette("ssh-key-not-implemented") do result = gitlab_net.authorized_key("whatever") - result.should be_nil + expect(result).to be_nil end end it "should return nil when the fingerprint is not found" do VCR.use_cassette("ssh-key-not-found") do result = gitlab_net.authorized_key("whatever") - result.should be_nil + expect(result).to be_nil end end it "should return a ssh key with a valid fingerprint" do VCR.use_cassette("ssh-key-ok") do result = gitlab_net.authorized_key(ssh_key) - result.should eq({ + expect(result).to eql({ "can_push" => false, "created_at" => "2017-06-21T09:50:07.150Z", "id" => 99, @@ -228,7 +234,7 @@ describe GitlabNet, vcr: true do describe '#two_factor_recovery_codes' do it 'returns two factor recovery codes' do VCR.use_cassette('two-factor-recovery-codes') do - result = gitlab_net.two_factor_recovery_codes(key) + result = gitlab_net.two_factor_recovery_codes(actor1) expect(result['success']).to be_truthy expect(result['recovery_codes']).to eq(['f67c514de60c4953','41278385fc00c1e0']) end @@ -236,7 +242,7 @@ describe GitlabNet, vcr: true do it 'returns false when recovery codes cannot be generated' do VCR.use_cassette('two-factor-recovery-codes-fail') do - result = gitlab_net.two_factor_recovery_codes('key-777') + result = gitlab_net.two_factor_recovery_codes(bad_actor1) expect(result['success']).to be_falsey expect(result['message']).to eq('Could not find the given key') end @@ -252,7 +258,7 @@ describe GitlabNet, vcr: true do it 'sets the arguments as form parameters' do VCR.use_cassette('notify-post-receive') do - Net::HTTP::Post.any_instance.should_receive(:set_form_data).with(hash_including(params)) + allow_any_instance_of(Net::HTTP::Post).to receive(:set_form_data).with(hash_including(params)) gitlab_net.notify_post_receive(gl_repository, repo_path) end end @@ -265,92 +271,159 @@ describe GitlabNet, vcr: true do end describe '#check_access' do + context 'something is wrong with the API response' do + context 'but response is JSON parsable' do + it 'raises an UnknownError exception' do + VCR.use_cassette('failed-push') do + expect do + gitlab_net.check_access('git-receive-pack', nil, project, actor1, changes, 'ssh') + end.to raise_error(UnknownError, 'API is not accessible: An internal server error occurred') + end + end + end + + context 'but response is not JSON parsable' do + it 'raises an UnknownError exception' do + VCR.use_cassette('failed-push-unparsable') do + expect do + gitlab_net.check_access('git-receive-pack', nil, project, actor1, changes, 'ssh') + end.to raise_error(UnknownError, 'API is not accessible') + end + end + end + end + context 'ssh key with access nil, to project' do - it 'should allow pull access for host' do - VCR.use_cassette("allowed-pull") do - access = gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'ssh') - access.allowed?.should be_truthy + it 'should allow push access for host' do + VCR.use_cassette('allowed-push') do + action = gitlab_net.check_access('git-receive-pack', nil, project, actor1, changes, 'ssh') + expect(action).to be_instance_of(Action::Gitaly) end end it 'adds the secret_token to the request' do - VCR.use_cassette("allowed-pull") do - Net::HTTP::Post.any_instance.should_receive(:set_form_data).with(hash_including(secret_token: secret)) - gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'ssh') + VCR.use_cassette('allowed-pull') do + allow_any_instance_of(Net::HTTP::Post).to receive(:set_form_data).with(hash_including(secret_token: secret)) + gitlab_net.check_access('git-receive-pack', nil, project, actor1, changes, 'ssh') end end - it 'should allow push access for host' do - VCR.use_cassette("allowed-push") do - access = gitlab_net.check_access('git-upload-pack', nil, project, key, changes, 'ssh') - access.allowed?.should be_truthy + it 'should allow pull access for host' do + VCR.use_cassette("allowed-pull") do + action = gitlab_net.check_access('git-upload-pack', nil, project, actor1, changes, 'ssh') + expect(action).to be_instance_of(Action::Gitaly) end end end context 'ssh access has been disabled' do it 'should deny pull access for host' do + VCR.use_cassette('ssh-pull-disabled-old') do + expect do + gitlab_net.check_access('git-upload-pack', nil, project, actor1, changes, 'http') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') + end + VCR.use_cassette('ssh-pull-disabled') do - access = gitlab_net.check_access('git-upload-pack', nil, project, key, changes, 'ssh') - access.allowed?.should be_falsey - access.message.should eq 'Git access over SSH is not allowed' + expect do + gitlab_net.check_access('git-upload-pack', nil, project, actor1, changes, 'http') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') end end it 'should deny push access for host' do + VCR.use_cassette('ssh-push-disabled-old') do + expect do + gitlab_net.check_access('git-receive-pack', nil, project, actor1, changes, 'ssh') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') + end + VCR.use_cassette('ssh-push-disabled') do - access = gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'ssh') - access.allowed?.should be_falsey - access.message.should eq 'Git access over SSH is not allowed' + expect do + gitlab_net.check_access('git-receive-pack', nil, project, actor1, changes, 'ssh') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') end end end context 'http access has been disabled' do it 'should deny pull access for host' do + VCR.use_cassette('http-pull-disabled-old') do + expect do + gitlab_net.check_access('git-upload-pack', nil, project, actor1, changes, 'http') + end.to raise_error(AccessDeniedError, 'Pulling over HTTP is not allowed.') + end + VCR.use_cassette('http-pull-disabled') do - access = gitlab_net.check_access('git-upload-pack', nil, project, key, changes, 'http') - access.allowed?.should be_falsey - access.message.should eq 'Pulling over HTTP is not allowed.' + expect do + gitlab_net.check_access('git-upload-pack', nil, project, actor1, changes, 'http') + end.to raise_error(AccessDeniedError, 'Pulling over HTTP is not allowed.') end end it 'should deny push access for host' do - VCR.use_cassette("http-push-disabled") do - access = gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'http') - access.allowed?.should be_falsey - access.message.should eq 'Pushing over HTTP is not allowed.' + VCR.use_cassette('http-push-disabled-old') do + expect do + gitlab_net.check_access('git-receive-pack', nil, project, actor1, changes, 'http') + end.to raise_error(AccessDeniedError, 'Pushing over HTTP is not allowed.') + end + + VCR.use_cassette('http-push-disabled') do + expect do + gitlab_net.check_access('git-receive-pack', nil, project, actor1, changes, 'http') + end.to raise_error(AccessDeniedError, 'Pushing over HTTP is not allowed.') end end end context 'ssh key without access to project' do it 'should deny pull access for host' do - VCR.use_cassette("ssh-pull-project-denied") do - access = gitlab_net.check_access('git-receive-pack', nil, project, key2, changes, 'ssh') - access.allowed?.should be_falsey + VCR.use_cassette('ssh-pull-project-denied-old') do + expect do + gitlab_net.check_access('git-receive-pack', nil, project, actor2, changes, 'ssh') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') + end + + VCR.use_cassette('ssh-pull-project-denied') do + expect do + gitlab_net.check_access('git-receive-pack', nil, project, actor2, changes, 'ssh') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') end end it 'should deny push access for host' do - VCR.use_cassette("ssh-push-project-denied") do - access = gitlab_net.check_access('git-upload-pack', nil, project, key2, changes, 'ssh') - access.allowed?.should be_falsey + VCR.use_cassette('ssh-push-project-denied-old') do + expect do + gitlab_net.check_access('git-upload-pack', nil, project, actor2, changes, 'ssh') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') + end + + VCR.use_cassette('ssh-push-project-denied') do + expect do + gitlab_net.check_access('git-upload-pack', nil, project, actor2, changes, 'ssh') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') end end it 'should deny push access for host (with user)' do - VCR.use_cassette("ssh-push-project-denied-with-user") do - access = gitlab_net.check_access('git-upload-pack', nil, project, 'user-2', changes, 'ssh') - access.allowed?.should be_falsey + VCR.use_cassette('ssh-push-project-denied-with-user-old') do + expect do + gitlab_net.check_access('git-upload-pack', nil, project, actor2, changes, 'ssh') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') + end + + VCR.use_cassette('ssh-push-project-denied-with-user') do + expect do + gitlab_net.check_access('git-upload-pack', nil, project, actor2, changes, 'ssh') + end.to raise_error(AccessDeniedError, 'Git access over SSH is not allowed') end end end it "raises an exception if the connection fails" do - Net::HTTP.any_instance.stub(:request).and_raise(StandardError) + allow_any_instance_of(Net::HTTP).to receive(:request).and_raise(StandardError) expect { - gitlab_net.check_access('git-upload-pack', nil, project, 'user-1', changes, 'ssh') + gitlab_net.check_access('git-upload-pack', nil, project, actor1, changes, 'ssh') }.to raise_error(GitlabNet::ApiUnreachableError) end end @@ -377,8 +450,8 @@ describe GitlabNet, vcr: true do subject { gitlab_net.send :http_client_for, URI('https://localhost/') } before do - gitlab_net.stub :cert_store - gitlab_net.send(:config).stub(:http_settings) { {'self_signed_cert' => true} } + allow(gitlab_net).to receive(:cert_store) + allow(gitlab_net.send(:config)).to receive(:http_settings).and_return({ 'self_signed_cert' => true }) end its(:verify_mode) { should eq(OpenSSL::SSL::VERIFY_NONE) } @@ -398,11 +471,11 @@ describe GitlabNet, vcr: true do subject { gitlab_net.send :http_request_for, :get, url } before do - gitlab_net.send(:config).http_settings.stub(:[]).with('user') { user } - gitlab_net.send(:config).http_settings.stub(:[]).with('password') { password } - Net::HTTP::Get.should_receive(:new).with('/', {}).and_return(get) - get.should_receive(:basic_auth).with(user, password).once - get.should_receive(:set_form_data).with(hash_including(secret_token: secret)).once + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('user').and_return(user) + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('password').and_return(password) + expect(Net::HTTP::Get).to receive(:new).with('/', {}).and_return(get) + expect(get).to receive(:basic_auth).with(user, password).once + expect(get).to receive(:set_form_data).with(hash_including(secret_token: secret)).once end it { should_not be_nil } @@ -412,11 +485,11 @@ describe GitlabNet, vcr: true do subject { gitlab_net.send :http_request_for, :get, url, params: params, headers: headers } before do - gitlab_net.send(:config).http_settings.stub(:[]).with('user') { user } - gitlab_net.send(:config).http_settings.stub(:[]).with('password') { password } - Net::HTTP::Get.should_receive(:new).with('/', headers).and_return(get) - get.should_receive(:basic_auth).with(user, password).once - get.should_receive(:set_form_data).with({ 'key1' => 'value1', secret_token: secret }).once + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('user').and_return(user) + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('password').and_return(password) + expect(Net::HTTP::Get).to receive(:new).with('/', headers).and_return(get) + expect(get).to receive(:basic_auth).with(user, password).once + expect(get).to receive(:set_form_data).with({ 'key1' => 'value1', secret_token: secret }).once end it { should_not be_nil } @@ -426,11 +499,11 @@ describe GitlabNet, vcr: true do subject { gitlab_net.send :http_request_for, :get, url, headers: headers } before do - gitlab_net.send(:config).http_settings.stub(:[]).with('user') { user } - gitlab_net.send(:config).http_settings.stub(:[]).with('password') { password } - Net::HTTP::Get.should_receive(:new).with('/', headers).and_return(get) - get.should_receive(:basic_auth).with(user, password).once - get.should_receive(:set_form_data).with(hash_including(secret_token: secret)).once + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('user').and_return(user) + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('password').and_return(password) + expect(Net::HTTP::Get).to receive(:new).with('/', headers).and_return(get) + expect(get).to receive(:basic_auth).with(user, password).once + expect(get).to receive(:set_form_data).with(hash_including(secret_token: secret)).once end it { should_not be_nil } @@ -441,12 +514,12 @@ describe GitlabNet, vcr: true do subject { gitlab_net.send :http_request_for, :get, url, options: options } before do - gitlab_net.send(:config).http_settings.stub(:[]).with('user') { user } - gitlab_net.send(:config).http_settings.stub(:[]).with('password') { password } - Net::HTTP::Get.should_receive(:new).with('/', {}).and_return(get) - get.should_receive(:basic_auth).with(user, password).once - get.should_receive(:body=).with({ 'key2' => 'value2', secret_token: secret }.to_json).once - get.should_not_receive(:set_form_data) + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('user').and_return(user) + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('password').and_return(password) + expect(Net::HTTP::Get).to receive(:new).with('/', {}).and_return(get) + expect(get).to receive(:basic_auth).with(user, password).once + expect(get).to receive(:body=).with({ 'key2' => 'value2', secret_token: secret }.to_json).once + expect(get).to_not receive(:set_form_data) end it { should_not be_nil } @@ -457,7 +530,7 @@ describe GitlabNet, vcr: true do context 'Unix socket' do it 'sets the Host header to "localhost"' do gitlab_net = described_class.new - gitlab_net.should_receive(:secret_token).and_return(secret) + expect(gitlab_net).to receive(:secret_token).and_return(secret) request = gitlab_net.send(:http_request_for, :get, URI('http+unix://%2Ffoo')) @@ -469,12 +542,12 @@ describe GitlabNet, vcr: true do describe '#cert_store' do let(:store) do double(OpenSSL::X509::Store).tap do |store| - OpenSSL::X509::Store.stub(:new) { store } + allow(OpenSSL::X509::Store).to receive(:new).and_return(store) end end before :each do - store.should_receive(:set_default_paths).once + expect(store).to receive(:set_default_paths).once end after do @@ -482,17 +555,17 @@ describe GitlabNet, vcr: true do end it "calls add_file with http_settings['ca_file']" do - gitlab_net.send(:config).http_settings.stub(:[]).with('ca_file') { 'test_file' } - gitlab_net.send(:config).http_settings.stub(:[]).with('ca_path') { nil } - store.should_receive(:add_file).with('test_file') - store.should_not_receive(:add_path) + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('ca_file').and_return('test_file') + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('ca_path').and_return(nil) + expect(store).to receive(:add_file).with('test_file') + expect(store).to_not receive(:add_path) end it "calls add_path with http_settings['ca_path']" do - gitlab_net.send(:config).http_settings.stub(:[]).with('ca_file') { nil } - gitlab_net.send(:config).http_settings.stub(:[]).with('ca_path') { 'test_path' } - store.should_not_receive(:add_file) - store.should_receive(:add_path).with('test_path') + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('ca_file').and_return(nil) + allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('ca_path').and_return('test_path') + expect(store).to_not receive(:add_file) + expect(store).to receive(:add_path).with('test_path') end end end diff --git a/spec/gitlab_post_receive_spec.rb b/spec/gitlab_post_receive_spec.rb index 46e6158..c41cceb 100644 --- a/spec/gitlab_post_receive_spec.rb +++ b/spec/gitlab_post_receive_spec.rb @@ -5,13 +5,13 @@ require 'gitlab_post_receive' describe GitlabPostReceive do let(:repository_path) { "/home/git/repositories" } let(:repo_name) { 'dzaporozhets/gitlab-ci' } - let(:actor) { 'key-123' } + let(:gl_id) { 'key-123' } let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } let(:repo_path) { File.join(repository_path, repo_name) + ".git" } let(:gl_repository) { "project-1" } - let(:gitlab_post_receive) { GitlabPostReceive.new(gl_repository, repo_path, actor, wrongly_encoded_changes) } + let(:gitlab_post_receive) { GitlabPostReceive.new(gl_repository, repo_path, gl_id, wrongly_encoded_changes) } let(:broadcast_message) { "test " * 10 + "message " * 10 } let(:enqueued_at) { Time.new(2016, 6, 23, 6, 59) } let(:new_merge_request_urls) do @@ -31,7 +31,7 @@ describe GitlabPostReceive do before do $logger = double('logger').as_null_object # Global vars are bad - GitlabConfig.any_instance.stub(repos_path: repository_path) + allow_any_instance_of(GitlabConfig).to receive(:repos_path).and_return(repository_path) end describe "#exec" do @@ -63,7 +63,7 @@ describe GitlabPostReceive do context 'when contains long url string at end' do let(:broadcast_message) { "test " * 10 + "message " * 10 + "https://localhost:5000/test/a/really/long/url/that/is/in/the/broadcast/message/do-not-truncate-when-url" } - it 'doesnt truncate url' do + it 'doesnt truncate url' do expect_any_instance_of(GitlabNet).to receive(:post_receive).and_return(response) assert_broadcast_message_printed_keep_long_url_end(gitlab_post_receive) assert_new_mr_printed(gitlab_post_receive) @@ -75,7 +75,7 @@ describe GitlabPostReceive do context 'when contains long url string at start' do let(:broadcast_message) { "https://localhost:5000/test/a/really/long/url/that/is/in/the/broadcast/message/do-not-truncate-when-url " + "test " * 10 + "message " * 11} - it 'doesnt truncate url' do + it 'doesnt truncate url' do expect_any_instance_of(GitlabNet).to receive(:post_receive).and_return(response) assert_broadcast_message_printed_keep_long_url_start(gitlab_post_receive) assert_new_mr_printed(gitlab_post_receive) @@ -87,7 +87,7 @@ describe GitlabPostReceive do context 'when contains long url string in middle' do let(:broadcast_message) { "test " * 11 + "https://localhost:5000/test/a/really/long/url/that/is/in/the/broadcast/message/do-not-truncate-when-url " + "message " * 11} - it 'doesnt truncate url' do + it 'doesnt truncate url' do expect_any_instance_of(GitlabNet).to receive(:post_receive).and_return(response) assert_broadcast_message_printed_keep_long_url_middle(gitlab_post_receive) assert_new_mr_printed(gitlab_post_receive) @@ -198,7 +198,7 @@ describe GitlabPostReceive do expect(gitlab_post_receive).to receive(:puts).with( " message message message message message message message message" ).ordered - + expect(gitlab_post_receive).to receive(:puts).with( "https://localhost:5000/test/a/really/long/url/that/is/in/the/broadcast/message/do-not-truncate-when-url" ).ordered @@ -215,7 +215,7 @@ describe GitlabPostReceive do "========================================================================" ).ordered expect(gitlab_post_receive).to receive(:puts).ordered - + expect(gitlab_post_receive).to receive(:puts).with( "https://localhost:5000/test/a/really/long/url/that/is/in/the/broadcast/message/do-not-truncate-when-url" ).ordered @@ -244,7 +244,7 @@ describe GitlabPostReceive do "========================================================================" ).ordered expect(gitlab_post_receive).to receive(:puts).ordered - + expect(gitlab_post_receive).to receive(:puts).with( " test test test test test test test test test test test" ).ordered diff --git a/spec/gitlab_shell_spec.rb b/spec/gitlab_shell_spec.rb index 382cad4..c46da5d 100644 --- a/spec/gitlab_shell_spec.rb +++ b/spec/gitlab_shell_spec.rb @@ -1,6 +1,6 @@ require_relative 'spec_helper' require_relative '../lib/gitlab_shell' -require_relative '../lib/gitlab_access_status' +require_relative '../lib/action' describe GitlabShell do before do @@ -8,565 +8,208 @@ describe GitlabShell do FileUtils.mkdir_p(tmp_repos_path) end - after do - FileUtils.rm_rf(tmp_repos_path) - end - - subject do - ARGV[0] = gl_id - GitlabShell.new(gl_id).tap do |shell| - shell.stub(exec_cmd: :exec_called) - shell.stub(api: api) - end - end + after { FileUtils.rm_rf(tmp_repos_path) } - let(:gitaly_check_access) { GitAccessStatus.new( - true, - 'ok', - gl_repository: gl_repository, - gl_id: gl_id, - gl_username: gl_username, - repository_path: repo_path, - gitaly: { 'repository' => { 'relative_path' => repo_name, 'storage_name' => 'default'} , 'address' => 'unix:gitaly.socket' }, - git_protocol: git_protocol - ) - } - - let(:api) do - double(GitlabNet).tap do |api| - api.stub(discover: { 'name' => 'John Doe', 'username' => 'testuser' }) - api.stub(check_access: GitAccessStatus.new( - true, - 'ok', - gl_repository: gl_repository, - gl_id: gl_id, - gl_username: gl_username, - repository_path: repo_path, - gitaly: nil, - git_protocol: git_protocol)) - api.stub(two_factor_recovery_codes: { - 'success' => true, - 'recovery_codes' => %w[f67c514de60c4953 41278385fc00c1e0] - }) - end - end + subject { described_class.new(who) } - let(:gl_id) { "key-#{rand(100) + 100}" } - let(:ssh_cmd) { nil } + let(:who) { 'key-1' } + let(:audit_usernames) { true } + let(:actor) { Actor.new_from(who, audit_usernames: audit_usernames) } let(:tmp_repos_path) { File.join(ROOT_PATH, 'tmp', 'repositories') } - let(:repo_name) { 'gitlab-ci.git' } let(:repo_path) { File.join(tmp_repos_path, repo_name) } let(:gl_repository) { 'project-1' } - let(:gl_id) { 'user-1' } let(:gl_username) { 'testuser' } let(:git_protocol) { 'version=2' } - before do - GitlabConfig.any_instance.stub(audit_usernames: false) - end + let(:api) { double(GitlabNet) } + let(:config) { double(GitlabConfig) } - describe :initialize do - let(:ssh_cmd) { 'git-receive-pack' } + let(:gitaly_action) { Action::Gitaly.new( + actor, + gl_repository, + gl_username, + git_protocol, + repo_path, + { 'repository' => { 'relative_path' => repo_name, 'storage_name' => 'default' } , 'address' => 'unix:gitaly.socket' }) + } + let(:api_2fa_recovery_action) { Action::API2FARecovery.new(actor) } + let(:git_lfs_authenticate_action) { Action::GitLFSAuthenticate.new(actor, repo_name) } - its(:gl_id) { should == gl_id } - end + before do + allow(GitlabConfig).to receive(:new).and_return(config) + allow(config).to receive(:audit_usernames).and_return(audit_usernames) - describe :parse_cmd do - describe 'git' do - context 'w/o namespace' do - let(:ssh_args) { %w(git-upload-pack gitlab-ci.git) } + allow(Actor).to receive(:new_from).with(who, audit_usernames: audit_usernames).and_return(actor) - before do - subject.send :parse_cmd, ssh_args - end + allow(GitlabNet).to receive(:new).and_return(api) + allow(api).to receive(:discover).with(actor).and_return('username' => gl_username) + end - its(:repo_name) { should == 'gitlab-ci.git' } - its(:command) { should == 'git-upload-pack' } + describe '#exec' do + context "when we don't have a valid user" do + before do + allow(api).to receive(:discover).with(actor).and_return(nil) end - context 'namespace' do - let(:repo_name) { 'dmitriy.zaporozhets/gitlab-ci.git' } - let(:ssh_args) { %w(git-upload-pack dmitriy.zaporozhets/gitlab-ci.git) } - - before do - subject.send :parse_cmd, ssh_args - end - - its(:repo_name) { should == 'dmitriy.zaporozhets/gitlab-ci.git' } - its(:command) { should == 'git-upload-pack' } + it 'prints Welcome.. and returns true' do + expect { + expect(subject.exec(nil)).to be_truthy + }.to output("Welcome to GitLab, Anonymous!\n").to_stdout end + end - context 'with an invalid number of arguments' do - let(:ssh_args) { %w(foobar) } - - it "should raise an DisallowedCommandError" do - expect { subject.send :parse_cmd, ssh_args }.to raise_error(GitlabShell::DisallowedCommandError) + context 'when we have a valid user' do + context 'when origin_cmd is nil' do + it 'prints Welcome.. and returns true' do + expect { + expect(subject.exec(nil)).to be_truthy + }.to output("Welcome to GitLab, @testuser!\n").to_stdout end end - context 'with an API command' do - before do - subject.send :parse_cmd, ssh_args - end - - context 'when generating recovery codes' do - let(:ssh_args) { %w(2fa_recovery_codes) } - - it 'sets the correct command' do - expect(subject.command).to eq('2fa_recovery_codes') - end - - it 'does not set repo name' do - expect(subject.repo_name).to be_nil - end + context 'when origin_cmd is empty' do + it 'prints Welcome.. and returns true' do + expect { + expect(subject.exec('')).to be_truthy + }.to output("Welcome to GitLab, @testuser!\n").to_stdout end end end - describe 'git-lfs' do - let(:repo_name) { 'dzaporozhets/gitlab.git' } - let(:ssh_args) { %w(git-lfs-authenticate dzaporozhets/gitlab.git download) } - - before do - subject.send :parse_cmd, ssh_args + context 'when origin_cmd is invalid' do + it 'prints a message to stderr and returns false' do + expect { + expect(subject.exec("git-invalid-command #{repo_name}")).to be_falsey + }.to output("GitLab: Disallowed command\n").to_stderr end - - its(:repo_name) { should == 'dzaporozhets/gitlab.git' } - its(:command) { should == 'git-lfs-authenticate' } - its(:git_access) { should == 'git-upload-pack' } end - describe 'git-lfs old clients' do - let(:repo_name) { 'dzaporozhets/gitlab.git' } - let(:ssh_args) { %w(git-lfs-authenticate dzaporozhets/gitlab.git download long_oid) } - - before do - subject.send :parse_cmd, ssh_args + context 'when origin_cmd is valid, but incomplete' do + it 'prints a message to stderr and returns false' do + expect { + expect(subject.exec('git-upload-pack')).to be_falsey + }.to output("GitLab: Disallowed command\n").to_stderr end - - its(:repo_name) { should == 'dzaporozhets/gitlab.git' } - its(:command) { should == 'git-lfs-authenticate' } - its(:git_access) { should == 'git-upload-pack' } - end - end - - describe :exec do - let(:gitaly_message) do - JSON.dump( - 'repository' => { 'relative_path' => repo_name, 'storage_name' => 'default' }, - 'gl_repository' => gl_repository, - 'gl_id' => gl_id, - 'gl_username' => gl_username, - 'git_protocol' => git_protocol - ) - end - - before do - allow(ENV).to receive(:[]).with('GIT_PROTOCOL').and_return(git_protocol) end - shared_examples_for 'upload-pack' do |command| - let(:ssh_cmd) { "#{command} gitlab-ci.git" } - after { subject.exec(ssh_cmd) } - - it "should process the command" do - subject.should_receive(:process_cmd).with(%w(git-upload-pack gitlab-ci.git)) - end - - it "should execute the command" do - subject.should_receive(:exec_cmd).with('git-upload-pack', repo_path) - end - - it "should log the command execution" do - message = "executing git command" - user_string = "user with id #{gl_id}" - $logger.should_receive(:info).with(message, command: "git-upload-pack #{repo_path}", user: user_string) - end - - it "should use usernames if configured to do so" do - GitlabConfig.any_instance.stub(audit_usernames: true) - $logger.should_receive(:info).with("executing git command", hash_including(user: 'testuser')) - end - end - - context 'git-upload-pack' do - it_behaves_like 'upload-pack', 'git-upload-pack' - end - - context 'git upload-pack' do - it_behaves_like 'upload-pack', 'git upload-pack' - end - - context 'gitaly-upload-pack' do - let(:ssh_cmd) { "git-upload-pack gitlab-ci.git" } - before do - api.stub(check_access: gitaly_check_access) - end - after { subject.exec(ssh_cmd) } - - it "should process the command" do - subject.should_receive(:process_cmd).with(%w(git-upload-pack gitlab-ci.git)) - end - - it "should execute the command" do - subject.should_receive(:exec_cmd).with(File.join(ROOT_PATH, "bin/gitaly-upload-pack"), 'unix:gitaly.socket', gitaly_message) - end - - it "should log the command execution" do - message = "executing git command" - user_string = "user with id #{gl_id}" - $logger.should_receive(:info).with(message, command: "gitaly-upload-pack unix:gitaly.socket #{gitaly_message}", user: user_string) + context 'when origin_cmd is git-lfs-authenticate' do + context 'but incomplete' do + it 'prints a message to stderr and returns false' do + expect { + expect(subject.exec('git-lfs-authenticate')).to be_falsey + }.to output("GitLab: Disallowed command\n").to_stderr + end end - it "should use usernames if configured to do so" do - GitlabConfig.any_instance.stub(audit_usernames: true) - $logger.should_receive(:info).with("executing git command", hash_including(user: 'testuser')) + context 'but invalid' do + it 'prints a message to stderr and returns false' do + expect { + expect(subject.exec("git-lfs-authenticate #{repo_name} invalid")).to be_falsey + }.to output("GitLab: Disallowed command\n").to_stderr + end end end - context 'git-receive-pack' do - let(:ssh_cmd) { "git-receive-pack gitlab-ci.git" } - after { subject.exec(ssh_cmd) } - - it "should process the command" do - subject.should_receive(:process_cmd).with(%w(git-receive-pack gitlab-ci.git)) - end - - it "should execute the command" do - subject.should_receive(:exec_cmd).with('git-receive-pack', repo_path) - end - - it "should log the command execution" do - message = "executing git command" - user_string = "user with id #{gl_id}" - $logger.should_receive(:info).with(message, command: "git-receive-pack #{repo_path}", user: user_string) - end - end + context 'when origin_cmd is 2fa_recovery_codes' do + let(:origin_cmd) { '2fa_recovery_codes' } + let(:git_access) { '2fa_recovery_codes' } - context 'gitaly-receive-pack' do - let(:ssh_cmd) { "git-receive-pack gitlab-ci.git" } before do - api.stub(check_access: gitaly_check_access) - end - after { subject.exec(ssh_cmd) } - - it "should process the command" do - subject.should_receive(:process_cmd).with(%w(git-receive-pack gitlab-ci.git)) - end - - it "should execute the command" do - subject.should_receive(:exec_cmd).with(File.join(ROOT_PATH, "bin/gitaly-receive-pack"), 'unix:gitaly.socket', gitaly_message) - end - - it "should log the command execution" do - message = "executing git command" - user_string = "user with id #{gl_id}" - $logger.should_receive(:info).with(message, command: "gitaly-receive-pack unix:gitaly.socket #{gitaly_message}", user: user_string) - end - - it "should use usernames if configured to do so" do - GitlabConfig.any_instance.stub(audit_usernames: true) - $logger.should_receive(:info).with("executing git command", hash_including(user: 'testuser')) - end - end - - shared_examples_for 'upload-archive' do |command| - let(:ssh_cmd) { "#{command} gitlab-ci.git" } - let(:exec_cmd_params) { ['git-upload-archive', repo_path] } - let(:exec_cmd_log_params) { exec_cmd_params } - - after { subject.exec(ssh_cmd) } - - it "should process the command" do - subject.should_receive(:process_cmd).with(%w(git-upload-archive gitlab-ci.git)) - end - - it "should execute the command" do - subject.should_receive(:exec_cmd).with(*exec_cmd_params) + expect(Action::API2FARecovery).to receive(:new).with(actor).and_return(api_2fa_recovery_action) end - it "should log the command execution" do - message = "executing git command" - user_string = "user with id #{gl_id}" - $logger.should_receive(:info).with(message, command: exec_cmd_log_params.join(' '), user: user_string) - end - - it "should use usernames if configured to do so" do - GitlabConfig.any_instance.stub(audit_usernames: true) - $logger.should_receive(:info).with("executing git command", hash_including(user: 'testuser')) + it 'returns true' do + expect(api_2fa_recovery_action).to receive(:execute).with('2fa_recovery_codes', %w{ 2fa_recovery_codes }).and_return(true) + expect(subject.exec(origin_cmd)).to be_truthy end end - context 'git-upload-archive' do - it_behaves_like 'upload-archive', 'git-upload-archive' - end - - context 'git upload-archive' do - it_behaves_like 'upload-archive', 'git upload-archive' - end - - context 'gitaly-upload-archive' do + context 'when access to the repo is denied' do before do - api.stub(check_access: gitaly_check_access) + expect(api).to receive(:check_access).with('git-upload-pack', nil, repo_name, actor, '_any').and_raise(AccessDeniedError, 'Sorry, access denied') end - it_behaves_like 'upload-archive', 'git-upload-archive' do - let(:gitaly_executable) { "gitaly-upload-archive" } - let(:exec_cmd_params) do - [ - File.join(ROOT_PATH, "bin", gitaly_executable), - 'unix:gitaly.socket', - gitaly_message - ] - end - let(:exec_cmd_log_params) do - [gitaly_executable, 'unix:gitaly.socket', gitaly_message] - end + it 'prints a message to stderr and returns false' do + expect($stderr).to receive(:puts).with('GitLab: Sorry, access denied') + expect(subject.exec("git-upload-pack #{repo_name}")).to be_falsey end end - context 'arbitrary command' do - let(:ssh_cmd) { 'arbitrary command' } - after { subject.exec(ssh_cmd) } - - it "should not process the command" do - subject.should_not_receive(:process_cmd) - end - - it "should not execute the command" do - subject.should_not_receive(:exec_cmd) - end - - it "should log the attempt" do - message = 'Denied disallowed command' - user_string = "user with id #{gl_id}" - $logger.should_receive(:warn).with(message, command: 'arbitrary command', user: user_string) - end - end - - context 'no command' do - after { subject.exec(nil) } - - it "should call api.discover" do - api.should_receive(:discover).with(gl_id) - end - end - - context "failed connection" do - let(:ssh_cmd) { 'git-upload-pack gitlab-ci.git' } - + context 'when the API is unavailable' do before do - api.stub(:check_access).and_raise(GitlabNet::ApiUnreachableError) - end - after { subject.exec(ssh_cmd) } - - it "should not process the command" do - subject.should_not_receive(:process_cmd) + expect(api).to receive(:check_access).with('git-upload-pack', nil, repo_name, actor, '_any').and_raise(GitlabNet::ApiUnreachableError) end - it "should not execute the command" do - subject.should_not_receive(:exec_cmd) + it 'prints a message to stderr and returns false' do + expect($stderr).to receive(:puts).with('GitLab: Failed to authorize your Git request: internal API unreachable') + expect(subject.exec("git-upload-pack #{repo_name}")).to be_falsey end end - context 'with an API command' do + context 'when access has been verified OK' do before do - allow(subject).to receive(:continue?).and_return(true) + expect(api).to receive(:check_access).with(git_access, nil, repo_name, actor, '_any').and_return(gitaly_action) end - context 'when generating recovery codes' do - let(:ssh_cmd) { '2fa_recovery_codes' } - after do - subject.exec(ssh_cmd) - end - - it 'does not call verify_access' do - expect(subject).not_to receive(:verify_access) - end - - it 'calls the corresponding method' do - expect(subject).to receive(:api_2fa_recovery_codes) - end + context 'when origin_cmd is git-upload-pack' do + let(:origin_cmd) { 'git-upload-pack' } + let(:git_access) { 'git-upload-pack' } - it 'outputs recovery codes' do - expect($stdout).to receive(:puts) - .with(/f67c514de60c4953\n41278385fc00c1e0/) + it 'returns true' do + expect(gitaly_action).to receive(:execute).with('git-upload-pack', %W{git-upload-pack #{repo_name}}).and_return(true) + expect(subject.exec("#{origin_cmd} #{repo_name}")).to be_truthy end - context 'when the process is unsuccessful' do - it 'displays the error to the user' do - api.stub(two_factor_recovery_codes: { - 'success' => false, - 'message' => 'Could not find the given key' - }) - - expect($stdout).to receive(:puts) - .with(/Could not find the given key/) + context 'but repo path is invalid' do + it 'prints a message to stderr and returns false' do + expect(gitaly_action).to receive(:execute).with('git-upload-pack', %W{git-upload-pack #{repo_name}}).and_raise(InvalidRepositoryPathError) + expect($stderr).to receive(:puts).with('GitLab: Invalid repository path') + expect(subject.exec("#{origin_cmd} #{repo_name}")).to be_falsey end end - end - end - end - - describe :validate_access do - let(:ssh_cmd) { "git-upload-pack gitlab-ci.git" } - describe 'check access with api' do - after { subject.exec(ssh_cmd) } - - it "should call api.check_access" do - api.should_receive(:check_access).with('git-upload-pack', nil, 'gitlab-ci.git', gl_id, '_any', 'ssh') - end - - it "should disallow access and log the attempt if check_access returns false status" do - api.stub(check_access: GitAccessStatus.new( - false, - 'denied', - gl_repository: nil, - gl_id: nil, - gl_username: nil, - repository_path: nil, - gitaly: nil, - git_protocol: nil)) - message = 'Access denied' - user_string = "user with id #{gl_id}" - $logger.should_receive(:warn).with(message, command: 'git-upload-pack gitlab-ci.git', user: user_string) - end - end - - describe 'set the repository path' do - context 'with a correct path' do - before { subject.exec(ssh_cmd) } - - its(:repo_path) { should == repo_path } - end - - context "with a path that doesn't match an absolute path" do - before do - File.stub(:absolute_path) { 'y/gitlab-ci.git' } - end - - it "refuses to assign the path" do - $stderr.should_receive(:puts).with("GitLab: Invalid repository path") - expect(subject.exec(ssh_cmd)).to be_falsey + context "but we're using an old git version for Windows 2.14" do + it 'returns true' do + expect(gitaly_action).to receive(:execute).with('git-upload-pack', %W{git-upload-pack #{repo_name}}).and_return(true) + expect(subject.exec("git upload-pack #{repo_name}")).to be_truthy #NOTE: 'git upload-pack' vs. 'git-upload-pack' + end end end - end - end - - describe :exec_cmd do - let(:shell) { GitlabShell.new(gl_id) } - let(:env) do - { - 'HOME' => ENV['HOME'], - 'PATH' => ENV['PATH'], - 'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'], - 'LANG' => ENV['LANG'], - 'GL_ID' => gl_id, - 'GL_PROTOCOL' => 'ssh', - 'GL_REPOSITORY' => gl_repository, - 'GL_USERNAME' => 'testuser' - } - end - let(:exec_options) { { unsetenv_others: true, chdir: ROOT_PATH } } - before do - Kernel.stub(:exec) - shell.gl_repository = gl_repository - shell.git_protocol = git_protocol - shell.instance_variable_set(:@username, gl_username) - end - - it "uses Kernel::exec method" do - Kernel.should_receive(:exec).with(env, 1, 2, exec_options).once - shell.send :exec_cmd, 1, 2 - end - - it "refuses to execute a lone non-array argument" do - expect { shell.send :exec_cmd, 1 }.to raise_error(GitlabShell::DisallowedCommandError) - end - - it "allows one argument if it is an array" do - Kernel.should_receive(:exec).with(env, [1, 2], exec_options).once - shell.send :exec_cmd, [1, 2] - end - - context "when specifying a git_tracing log file" do - let(:git_trace_log_file) { '/tmp/git_trace_performance.log' } - - before do - GitlabConfig.any_instance.stub(git_trace_log_file: git_trace_log_file) - shell - end - - it "uses GIT_TRACE_PERFORMANCE" do - expected_hash = hash_including( - 'GIT_TRACE' => git_trace_log_file, - 'GIT_TRACE_PACKET' => git_trace_log_file, - 'GIT_TRACE_PERFORMANCE' => git_trace_log_file - ) - Kernel.should_receive(:exec).with(expected_hash, [1, 2], exec_options).once - - shell.send :exec_cmd, [1, 2] - end - - context "when provides a relative path" do - let(:git_trace_log_file) { 'git_trace_performance.log' } - - it "does not uses GIT_TRACE*" do - # If we try to use it we'll show a warning to the users - expected_hash = hash_excluding( - 'GIT_TRACE', 'GIT_TRACE_PACKET', 'GIT_TRACE_PERFORMANCE' - ) - Kernel.should_receive(:exec).with(expected_hash, [1, 2], exec_options).once - - shell.send :exec_cmd, [1, 2] - end - it "writes an entry on the log" do - message = 'git trace log path must be absolute, ignoring' + context 'when origin_cmd is git-lfs-authenticate' do + let(:origin_cmd) { 'git-lfs-authenticate' } + let(:lfs_access) { double(GitlabLfsAuthentication, authentication_payload: fake_payload)} - expect($logger).to receive(:warn). - with(message, git_trace_log_file: git_trace_log_file) - - Kernel.should_receive(:exec).with(env, [1, 2], exec_options).once - shell.send :exec_cmd, [1, 2] - end - end - - context "when provides a file not writable" do before do - expect(File).to receive(:open).with(git_trace_log_file, 'a').and_raise(Errno::EACCES) + expect(Action::GitLFSAuthenticate).to receive(:new).with(actor, repo_name).and_return(git_lfs_authenticate_action) end - it "does not uses GIT_TRACE*" do - # If we try to use it we'll show a warning to the users - expected_hash = hash_excluding( - 'GIT_TRACE', 'GIT_TRACE_PACKET', 'GIT_TRACE_PERFORMANCE' - ) - Kernel.should_receive(:exec).with(expected_hash, [1, 2], exec_options).once + context 'upload' do + let(:git_access) { 'git-receive-pack' } - shell.send :exec_cmd, [1, 2] + it 'returns true' do + expect(git_lfs_authenticate_action).to receive(:execute).with('git-lfs-authenticate', %w{ git-lfs-authenticate gitlab-ci.git upload }).and_return(true) + expect(subject.exec("#{origin_cmd} #{repo_name} upload")).to be_truthy + end end - it "writes an entry on the log" do - message = 'Failed to open git trace log file' - error = 'Permission denied' + context 'download' do + let(:git_access) { 'git-upload-pack' } - expect($logger).to receive(:warn). - with(message, git_trace_log_file: git_trace_log_file, error: error) + it 'returns true' do + expect(git_lfs_authenticate_action).to receive(:execute).with('git-lfs-authenticate', %w{ git-lfs-authenticate gitlab-ci.git download }).and_return(true) + expect(subject.exec("#{origin_cmd} #{repo_name} download")).to be_truthy + end - Kernel.should_receive(:exec).with(env, [1, 2], exec_options).once - shell.send :exec_cmd, [1, 2] + context 'for old git-lfs clients' do + it 'returns true' do + expect(git_lfs_authenticate_action).to receive(:execute).with('git-lfs-authenticate', %w{ git-lfs-authenticate gitlab-ci.git download long_oid }).and_return(true) + expect(subject.exec("#{origin_cmd} #{repo_name} download long_oid")).to be_truthy + end + end end end end end - - describe :api do - let(:shell) { GitlabShell.new(gl_id) } - subject { shell.send :api } - - it { should be_a(GitlabNet) } - end end diff --git a/spec/names_helper_spec.rb b/spec/names_helper_spec.rb index f2a95e5..11383da 100644 --- a/spec/names_helper_spec.rb +++ b/spec/names_helper_spec.rb @@ -5,8 +5,8 @@ describe NamesHelper do include NamesHelper describe :extract_ref_name do - it { extract_ref_name('refs/heads/awesome-feature').should == 'awesome-feature' } - it { extract_ref_name('refs/tags/v2.2.1').should == 'v2.2.1' } - it { extract_ref_name('refs/tags/releases/v2.2.1').should == 'releases/v2.2.1' } + it { expect(extract_ref_name('refs/heads/awesome-feature')).to eql 'awesome-feature' } + it { expect(extract_ref_name('refs/tags/v2.2.1')).to eql 'v2.2.1' } + it { expect(extract_ref_name('refs/tags/releases/v2.2.1')).to eql 'releases/v2.2.1' } end end diff --git a/spec/vcr_cassettes/allowed-pull.yml b/spec/vcr_cassettes/allowed-pull.yml index 1636fd5..0b22d7a 100644 --- a/spec/vcr_cassettes/allowed-pull.yml +++ b/spec/vcr_cassettes/allowed-pull.yml @@ -5,7 +5,7 @@ http_interactions: uri: http://localhost:3000/api/v4/internal/allowed body: encoding: US-ASCII - string: action=git-receive-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + string: action=git-upload-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A headers: Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 diff --git a/spec/vcr_cassettes/allowed-push.yml b/spec/vcr_cassettes/allowed-push.yml index b073476..bea28bb 100644 --- a/spec/vcr_cassettes/allowed-push.yml +++ b/spec/vcr_cassettes/allowed-push.yml @@ -5,7 +5,7 @@ http_interactions: uri: http://localhost:3000/api/v4/internal/allowed body: encoding: US-ASCII - string: action=git-upload-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + string: action=git-receive-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A headers: Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 diff --git a/spec/vcr_cassettes/discover-not-found.yml b/spec/vcr_cassettes/discover-not-found.yml new file mode 100644 index 0000000..db03b02 --- /dev/null +++ b/spec/vcr_cassettes/discover-not-found.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: get + uri: http://localhost:3000/api/v4/internal/discover?key_id=1 + body: + encoding: US-ASCII + string: secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 404 + message: Not Found + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '42' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 10:44:49 GMT + Etag: + - W/"63b4ab301951bea83c4fc398eba8e307" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - dc11b8d4-1972-417b-8305-2c35c849405c + X-Runtime: + - '0.230170' + body: + encoding: UTF-8 + string: '{"message":"404 Not found"}' + http_version: + recorded_at: Wed, 21 Jun 2017 10:44:49 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/failed-push-unparsable.yml b/spec/vcr_cassettes/failed-push-unparsable.yml new file mode 100644 index 0000000..fbc09ec --- /dev/null +++ b/spec/vcr_cassettes/failed-push-unparsable.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-upload-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 500 + message: Internal Server Error + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '155' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 10:44:52 GMT + Etag: + - W/"45654cae433b5a9c5fbba1d45d382e52" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 67ab4954-19e6-42ce-aae6-55c8ae5a365e + X-Runtime: + - '0.230871' + body: + encoding: UTF-8 + string: '""' + http_version: + recorded_at: Wed, 21 Jun 2017 10:44:52 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/failed-push.yml b/spec/vcr_cassettes/failed-push.yml new file mode 100644 index 0000000..334aabd --- /dev/null +++ b/spec/vcr_cassettes/failed-push.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-upload-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 500 + message: Internal Server Error + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '155' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 10:44:52 GMT + Etag: + - W/"45654cae433b5a9c5fbba1d45d382e52" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 67ab4954-19e6-42ce-aae6-55c8ae5a365e + X-Runtime: + - '0.230871' + body: + encoding: UTF-8 + string: '{"status":false,"message":"An internal server error occurred"}' + http_version: + recorded_at: Wed, 21 Jun 2017 10:44:52 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/http-pull-disabled-old.yml b/spec/vcr_cassettes/http-pull-disabled-old.yml new file mode 100644 index 0000000..23ef3e5 --- /dev/null +++ b/spec/vcr_cassettes/http-pull-disabled-old.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-upload-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=http&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '62' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 10:32:01 GMT + Etag: + - W/"71e09fcf8a60a03cd1acc22806386ead" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 70bdecc9-0078-4a4b-aa6b-cac1b2578886 + X-Runtime: + - '0.324202' + body: + encoding: UTF-8 + string: '{"status":false,"message":"Pulling over HTTP is not allowed."}' + http_version: + recorded_at: Wed, 21 Jun 2017 10:32:01 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/http-pull-disabled.yml b/spec/vcr_cassettes/http-pull-disabled.yml index 23ef3e5..34cf634 100644 --- a/spec/vcr_cassettes/http-pull-disabled.yml +++ b/spec/vcr_cassettes/http-pull-disabled.yml @@ -17,8 +17,8 @@ http_interactions: - application/x-www-form-urlencoded response: status: - code: 200 - message: OK + code: 401 + message: Unauthorized headers: Cache-Control: - max-age=0, private, must-revalidate diff --git a/spec/vcr_cassettes/http-push-disabled-old.yml b/spec/vcr_cassettes/http-push-disabled-old.yml new file mode 100644 index 0000000..676e5b8 --- /dev/null +++ b/spec/vcr_cassettes/http-push-disabled-old.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-receive-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=http&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '62' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 10:32:01 GMT + Etag: + - W/"7f14e23ac07cc8b0a53c567fcf9432fd" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 573f3584-87c6-41cb-a5bf-5e7ee76d4250 + X-Runtime: + - '0.266135' + body: + encoding: UTF-8 + string: '{"status":false,"message":"Pushing over HTTP is not allowed."}' + http_version: + recorded_at: Wed, 21 Jun 2017 10:32:01 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/http-push-disabled.yml b/spec/vcr_cassettes/http-push-disabled.yml index 676e5b8..e7e185e 100644 --- a/spec/vcr_cassettes/http-push-disabled.yml +++ b/spec/vcr_cassettes/http-push-disabled.yml @@ -17,8 +17,8 @@ http_interactions: - application/x-www-form-urlencoded response: status: - code: 200 - message: OK + code: 401 + message: Unauthorized headers: Cache-Control: - max-age=0, private, must-revalidate diff --git a/spec/vcr_cassettes/ssh-pull-disabled-old.yml b/spec/vcr_cassettes/ssh-pull-disabled-old.yml new file mode 100644 index 0000000..55ce261 --- /dev/null +++ b/spec/vcr_cassettes/ssh-pull-disabled-old.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-upload-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '63' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 12:23:57 GMT + Etag: + - W/"76a32010244f80700d5e1ba8a55d094c" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 096ae253-c6fe-4360-b4d4-48f4b5435ca6 + X-Runtime: + - '6.377187' + body: + encoding: UTF-8 + string: '{"status":false,"message":"Git access over SSH is not allowed"}' + http_version: + recorded_at: Wed, 21 Jun 2017 12:23:57 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/ssh-pull-disabled.yml b/spec/vcr_cassettes/ssh-pull-disabled.yml index 55ce261..92fc36a 100644 --- a/spec/vcr_cassettes/ssh-pull-disabled.yml +++ b/spec/vcr_cassettes/ssh-pull-disabled.yml @@ -17,8 +17,8 @@ http_interactions: - application/x-www-form-urlencoded response: status: - code: 200 - message: OK + code: 401 + message: Unauthorized headers: Cache-Control: - max-age=0, private, must-revalidate diff --git a/spec/vcr_cassettes/ssh-pull-project-denied-old.yml b/spec/vcr_cassettes/ssh-pull-project-denied-old.yml new file mode 100644 index 0000000..c16e608 --- /dev/null +++ b/spec/vcr_cassettes/ssh-pull-project-denied-old.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-receive-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=2&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '63' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 12:24:04 GMT + Etag: + - W/"76a32010244f80700d5e1ba8a55d094c" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - c843a5a3-fc08-46eb-aa45-caceae515638 + X-Runtime: + - '7.359835' + body: + encoding: UTF-8 + string: '{"status":false,"message":"Git access over SSH is not allowed"}' + http_version: + recorded_at: Wed, 21 Jun 2017 12:24:04 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/ssh-pull-project-denied.yml b/spec/vcr_cassettes/ssh-pull-project-denied.yml index c16e608..61a71db 100644 --- a/spec/vcr_cassettes/ssh-pull-project-denied.yml +++ b/spec/vcr_cassettes/ssh-pull-project-denied.yml @@ -17,8 +17,8 @@ http_interactions: - application/x-www-form-urlencoded response: status: - code: 200 - message: OK + code: 401 + message: Unauthorized headers: Cache-Control: - max-age=0, private, must-revalidate diff --git a/spec/vcr_cassettes/ssh-push-disabled-old.yml b/spec/vcr_cassettes/ssh-push-disabled-old.yml new file mode 100644 index 0000000..c061791 --- /dev/null +++ b/spec/vcr_cassettes/ssh-push-disabled-old.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-receive-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=1&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '63' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 12:23:57 GMT + Etag: + - W/"76a32010244f80700d5e1ba8a55d094c" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 93620e06-fda9-4be5-855e-300f5d62fa3c + X-Runtime: + - '0.207159' + body: + encoding: UTF-8 + string: '{"status":false,"message":"Git access over SSH is not allowed"}' + http_version: + recorded_at: Wed, 21 Jun 2017 12:23:57 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/ssh-push-disabled.yml b/spec/vcr_cassettes/ssh-push-disabled.yml index c061791..5ca2642 100644 --- a/spec/vcr_cassettes/ssh-push-disabled.yml +++ b/spec/vcr_cassettes/ssh-push-disabled.yml @@ -17,8 +17,8 @@ http_interactions: - application/x-www-form-urlencoded response: status: - code: 200 - message: OK + code: 401 + message: Unauthorized headers: Cache-Control: - max-age=0, private, must-revalidate diff --git a/spec/vcr_cassettes/ssh-push-project-denied-old.yml b/spec/vcr_cassettes/ssh-push-project-denied-old.yml new file mode 100644 index 0000000..5107d15 --- /dev/null +++ b/spec/vcr_cassettes/ssh-push-project-denied-old.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-upload-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&key_id=2&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '63' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 12:24:04 GMT + Etag: + - W/"76a32010244f80700d5e1ba8a55d094c" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 8ce54f29-9ed0-46e5-aedb-37edaa3d52da + X-Runtime: + - '0.228256' + body: + encoding: UTF-8 + string: '{"status":false,"message":"Git access over SSH is not allowed"}' + http_version: + recorded_at: Wed, 21 Jun 2017 12:24:04 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/ssh-push-project-denied-with-user-old.yml b/spec/vcr_cassettes/ssh-push-project-denied-with-user-old.yml new file mode 100644 index 0000000..b461b5b --- /dev/null +++ b/spec/vcr_cassettes/ssh-push-project-denied-with-user-old.yml @@ -0,0 +1,46 @@ +--- +http_interactions: +- request: + method: post + uri: http://localhost:3000/api/v4/internal/allowed + body: + encoding: US-ASCII + string: action=git-upload-pack&changes=0000000000000000000000000000000000000000+92d0970eefd7acb6d548878925ce2208cfe2d2ec+refs%2Fheads%2Fbranch4&gl_repository&project=gitlab-org%2Fgitlab-test.git&protocol=ssh&env=%7B%7D&user_id=2&secret_token=0a3938d9d95d807e94d937af3a4fbbea%0A + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '63' + Content-Type: + - application/json + Date: + - Wed, 21 Jun 2017 12:24:05 GMT + Etag: + - W/"76a32010244f80700d5e1ba8a55d094c" + Vary: + - Origin + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 3b242d73-d860-48ac-8fef-80e2d0d3daca + X-Runtime: + - '0.342469' + body: + encoding: UTF-8 + string: '{"status":false,"message":"Git access over SSH is not allowed"}' + http_version: + recorded_at: Wed, 21 Jun 2017 12:24:05 GMT +recorded_with: VCR 2.4.0 diff --git a/spec/vcr_cassettes/ssh-push-project-denied-with-user.yml b/spec/vcr_cassettes/ssh-push-project-denied-with-user.yml index b461b5b..01be21b 100644 --- a/spec/vcr_cassettes/ssh-push-project-denied-with-user.yml +++ b/spec/vcr_cassettes/ssh-push-project-denied-with-user.yml @@ -17,8 +17,8 @@ http_interactions: - application/x-www-form-urlencoded response: status: - code: 200 - message: OK + code: 401 + message: Unauthorized headers: Cache-Control: - max-age=0, private, must-revalidate diff --git a/spec/vcr_cassettes/ssh-push-project-denied.yml b/spec/vcr_cassettes/ssh-push-project-denied.yml index 5107d15..b23dd8d 100644 --- a/spec/vcr_cassettes/ssh-push-project-denied.yml +++ b/spec/vcr_cassettes/ssh-push-project-denied.yml @@ -17,8 +17,8 @@ http_interactions: - application/x-www-form-urlencoded response: status: - code: 200 - message: OK + code: 401 + message: Unauthorized headers: Cache-Control: - max-age=0, private, must-revalidate |