Commit 6cf55e51 authored by Mark Lapierre's avatar Mark Lapierre Committed by Mark Lapierre

Add an SSH key and use it to clone and push

Adds 2 end-to-end tests:

1. Add and remove an SSH key
2. Add an SSH key and use it to clone and push

Includes changes to factories to allow Git actions via SSH
parent 265b4913
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
- if project_nav_tab? :pipelines - if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines' do
.nav-icon-container .nav-icon-container
= sprite_icon('rocket') = sprite_icon('rocket')
%span.nav-item-name %span.nav-item-name
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
.form-group .form-group
= f.label :key, class: 'label-bold' = f.label :key, class: 'label-bold'
%p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.") %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
= f.text_area :key, class: "form-control js-add-ssh-key-validation-input", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"') = f.text_area :key, class: "form-control js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"')
.form-group .form-group
= f.label :title, class: 'label-bold' = f.label :title, class: 'label-bold'
= f.text_field :title, class: "form-control input-lg", required: true, placeholder: s_('Profiles|e.g. My MacBook key') = f.text_field :title, class: "form-control input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
%p.form-text.text-muted= _('Name your individual key via a title') %p.form-text.text-muted= _('Name your individual key via a title')
.js-add-ssh-key-validation-warning.hide .js-add-ssh-key-validation-warning.hide
...@@ -19,4 +19,4 @@ ...@@ -19,4 +19,4 @@
%button.btn.btn-create.js-add-ssh-key-validation-confirm-submit= _("Yes, add it") %button.btn.btn-create.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
.prepend-top-default .prepend-top-default
= f.submit s_('Profiles|Add key'), class: "btn btn-create js-add-ssh-key-validation-original-submit" = f.submit s_('Profiles|Add key'), class: "btn btn-create js-add-ssh-key-validation-original-submit qa-add-key-button"
...@@ -24,4 +24,4 @@ ...@@ -24,4 +24,4 @@
= @key.key = @key.key
.col-md-12 .col-md-12
.float-right .float-right
= link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button"
...@@ -57,6 +57,7 @@ module QA ...@@ -57,6 +57,7 @@ module QA
autoload :Wiki, 'qa/factory/resource/wiki' autoload :Wiki, 'qa/factory/resource/wiki'
autoload :File, 'qa/factory/resource/file' autoload :File, 'qa/factory/resource/file'
autoload :Fork, 'qa/factory/resource/fork' autoload :Fork, 'qa/factory/resource/fork'
autoload :SSHKey, 'qa/factory/resource/ssh_key'
end end
module Repository module Repository
...@@ -217,6 +218,7 @@ module QA ...@@ -217,6 +218,7 @@ module QA
module Profile module Profile
autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens' autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
autoload :SSHKeys, 'qa/page/profile/ssh_keys'
end end
module Issuable module Issuable
......
...@@ -11,7 +11,9 @@ module QA ...@@ -11,7 +11,9 @@ module QA
factory.output factory.output
end end
product(:project) { |factory| factory.project } product :project do |factory|
factory.project
end
def initialize def initialize
@file_name = 'file.txt' @file_name = 'file.txt'
...@@ -21,8 +23,8 @@ module QA ...@@ -21,8 +23,8 @@ module QA
@new_branch = true @new_branch = true
end end
def repository_uri def repository_http_uri
@repository_uri ||= begin @repository_http_uri ||= begin
project.visit! project.visit!
Page::Project::Show.act do Page::Project::Show.act do
choose_repository_clone_http choose_repository_clone_http
...@@ -30,6 +32,16 @@ module QA ...@@ -30,6 +32,16 @@ module QA
end end
end end
end end
def repository_ssh_uri
@repository_ssh_uri ||= begin
project.visit!
Page::Project::Show.act do
choose_repository_clone_ssh
repository_location.uri
end
end
end
end end
end end
end end
......
...@@ -5,8 +5,8 @@ module QA ...@@ -5,8 +5,8 @@ module QA
module Repository module Repository
class Push < Factory::Base class Push < Factory::Base
attr_accessor :file_name, :file_content, :commit_message, attr_accessor :file_name, :file_content, :commit_message,
:branch_name, :new_branch, :output, :repository_uri, :branch_name, :new_branch, :output, :repository_http_uri,
:user :repository_ssh_uri, :ssh_key, :user
attr_writer :remote_branch attr_writer :remote_branch
...@@ -16,7 +16,8 @@ module QA ...@@ -16,7 +16,8 @@ module QA
@commit_message = "This is a test commit" @commit_message = "This is a test commit"
@branch_name = 'master' @branch_name = 'master'
@new_branch = true @new_branch = true
@repository_uri = "" @repository_http_uri = ""
@ssh_key = nil
end end
def remote_branch def remote_branch
...@@ -31,9 +32,14 @@ module QA ...@@ -31,9 +32,14 @@ module QA
def fabricate! def fabricate!
Git::Repository.perform do |repository| Git::Repository.perform do |repository|
repository.uri = repository_uri if ssh_key
repository.uri = repository_ssh_uri
repository.use_ssh_key(ssh_key)
else
repository.uri = repository_http_uri
repository.use_default_credentials repository.use_default_credentials
end
username = 'GitLab QA' username = 'GitLab QA'
email = 'root@gitlab.com' email = 'root@gitlab.com'
...@@ -63,6 +69,8 @@ module QA ...@@ -63,6 +69,8 @@ module QA
repository.commit(commit_message) repository.commit(commit_message)
@output = repository.push_changes("#{branch_name}:#{remote_branch}") @output = repository.push_changes("#{branch_name}:#{remote_branch}")
repository.delete_ssh_key
end end
end end
end end
......
...@@ -16,8 +16,8 @@ module QA ...@@ -16,8 +16,8 @@ module QA
@new_branch = false @new_branch = false
end end
def repository_uri def repository_http_uri
@repository_uri ||= begin @repository_http_uri ||= begin
wiki.visit! wiki.visit!
Page::Project::Wiki::Show.act do Page::Project::Wiki::Show.act do
go_to_clone_repository go_to_clone_repository
......
...@@ -20,6 +20,13 @@ module QA ...@@ -20,6 +20,13 @@ module QA
end end
end end
product :repository_http_location do
Page::Project::Show.act do
choose_repository_clone_http
repository_location
end
end
def initialize def initialize
@description = 'My awesome project' @description = 'My awesome project'
end end
......
# frozen_string_literal: true
module QA
module Factory
module Resource
class SSHKey < Factory::Base
extend Forwardable
attr_accessor :title
attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint
product :private_key do |factory|
factory.private_key
end
product :title do |factory|
factory.title
end
product :fingerprint do |factory|
factory.fingerprint
end
def key
@key ||= Runtime::Key::RSA.new
end
def fabricate!
Page::Menu::Main.act { go_to_profile_settings }
Page::Menu::Profile.act { click_ssh_keys }
Page::Profile::SSHKeys.perform do |page|
page.add_key(public_key, title)
end
end
end
end
end
end
...@@ -7,6 +7,10 @@ module QA ...@@ -7,6 +7,10 @@ module QA
class Repository class Repository
include Scenario::Actable include Scenario::Actable
def initialize
@ssh_cmd = ""
end
def self.perform(*args) def self.perform(*args)
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
Dir.chdir(dir) { super } Dir.chdir(dir) { super }
...@@ -33,7 +37,7 @@ module QA ...@@ -33,7 +37,7 @@ module QA
end end
def clone(opts = '') def clone(opts = '')
run_and_redact_credentials("git clone #{opts} #{@uri} ./") run_and_redact_credentials(build_git_command("git clone #{opts} #{@uri} ./"))
end end
def checkout(branch_name) def checkout(branch_name)
...@@ -53,6 +57,10 @@ module QA ...@@ -53,6 +57,10 @@ module QA
`git config user.email #{email}` `git config user.email #{email}`
end end
def configure_ssh_command(command)
@ssh_cmd = "GIT_SSH_COMMAND='#{command}'"
end
def commit_file(name, contents, message) def commit_file(name, contents, message)
add_file(name, contents) add_file(name, contents)
commit(message) commit(message)
...@@ -69,7 +77,7 @@ module QA ...@@ -69,7 +77,7 @@ module QA
end end
def push_changes(branch = 'master') def push_changes(branch = 'master')
output, _ = run_and_redact_credentials("git push #{@uri} #{branch}") output, _ = run_and_redact_credentials(build_git_command("git push #{@uri} #{branch}"))
output output
end end
...@@ -78,6 +86,31 @@ module QA ...@@ -78,6 +86,31 @@ module QA
`git log --oneline`.split("\n") `git log --oneline`.split("\n")
end end
def use_ssh_key(key)
@private_key_file = Tempfile.new("id_#{SecureRandom.hex(8)}")
File.binwrite(@private_key_file, key.private_key)
File.chmod(0700, @private_key_file)
@known_hosts_file = Tempfile.new("known_hosts_#{SecureRandom.hex(8)}")
keyscan_params = ['-H']
keyscan_params << "-p #{@uri.port}" if @uri.port
keyscan_params << @uri.host
run_and_redact_credentials("ssh-keyscan #{keyscan_params.join(' ')} >> #{@known_hosts_file.path}")
configure_ssh_command("ssh -i #{@private_key_file.path} -o UserKnownHostsFile=#{@known_hosts_file.path}")
end
def delete_ssh_key
return unless @private_key_file
@private_key_file.close(true)
@known_hosts_file.close(true)
end
def build_git_command(command_str)
[@ssh_cmd, command_str].compact.join(' ')
end
private private
# Since the remote URL contains the credentials, and git occasionally # Since the remote URL contains the credentials, and git occasionally
......
...@@ -6,6 +6,7 @@ module QA ...@@ -6,6 +6,7 @@ module QA
element :access_token_link, 'link_to profile_personal_access_tokens_path' element :access_token_link, 'link_to profile_personal_access_tokens_path'
element :access_token_title, 'Access Tokens' element :access_token_title, 'Access Tokens'
element :top_level_items, '.sidebar-top-level-items' element :top_level_items, '.sidebar-top-level-items'
element :ssh_keys, 'SSH Keys'
end end
def click_access_tokens def click_access_tokens
...@@ -14,6 +15,12 @@ module QA ...@@ -14,6 +15,12 @@ module QA
end end
end end
def click_ssh_keys
within_sidebar do
click_link('SSH Keys')
end
end
private private
def within_sidebar def within_sidebar
......
...@@ -6,6 +6,7 @@ module QA ...@@ -6,6 +6,7 @@ module QA
element :settings_item element :settings_item
element :settings_link, 'link_to edit_project_path' element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: _('Repository')" element :repository_link, "title: _('Repository')"
element :link_pipelines
element :pipelines_settings_link, "title: _('CI / CD')" element :pipelines_settings_link, "title: _('CI / CD')"
element :operations_kubernetes_link, "title: _('Kubernetes')" element :operations_kubernetes_link, "title: _('Kubernetes')"
element :issues_link, /link_to.*shortcuts-issues/ element :issues_link, /link_to.*shortcuts-issues/
...@@ -49,7 +50,7 @@ module QA ...@@ -49,7 +50,7 @@ module QA
def click_ci_cd_pipelines def click_ci_cd_pipelines
within_sidebar do within_sidebar do
click_link('CI / CD') click_element :link_pipelines
end end
end end
......
# frozen_string_literal: true
module QA
module Page
module Profile
class SSHKeys < Page::Base
view 'app/views/profiles/keys/_form.html.haml' do
element :key_title_field
element :key_public_key_field
element :add_key_button
end
view 'app/views/profiles/keys/_key_details.html.haml' do
element :delete_key_button
end
def add_key(public_key, title)
fill_element :key_public_key_field, public_key
fill_element :key_title_field, title
click_element :add_key_button
end
def remove_key(title)
click_link(title)
accept_alert do
click_element :delete_key_button
end
end
end
end
end
end
# frozen_string_literal: true
module QA
context :create do
describe 'SSH keys support' do
let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
it 'user adds and then removes an SSH key' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
key = Factory::Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
end
expect(page).to have_content("Title: #{key_title}")
expect(page).to have_content(key.fingerprint)
Page::Menu::Main.act { go_to_profile_settings }
Page::Menu::Profile.act { click_ssh_keys }
Page::Profile::SSHKeys.perform do |ssh_keys|
ssh_keys.remove_key(key_title)
end
expect(page).not_to have_content("Title: #{key_title}")
expect(page).not_to have_content(key.fingerprint)
end
end
end
end
# frozen_string_literal: true
module QA
context :create do
describe 'SSH key support' do
# Note: If you run this test against GDK make sure you've enabled sshd
# See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md
let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
it 'user adds an ssh key and pushes code to the repository' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
key = Factory::Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
end
Factory::Repository::ProjectPush.fabricate! do |push|
push.ssh_key = key
push.file_name = 'README.md'
push.file_content = '# Test Use SSH Key'
push.commit_message = 'Add README.md'
end
Page::Project::Show.act { wait_for_push }
expect(page).to have_content('README.md')
expect(page).to have_content('Test Use SSH Key')
Page::Menu::Main.act { go_to_profile_settings }
Page::Menu::Profile.act { click_ssh_keys }
Page::Profile::SSHKeys.perform do |ssh_keys|
ssh_keys.remove_key(key_title)
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment