Merge branch 'feature/gpg-signed-commits' into 'master'
GPG signed commits Closes #20268 See merge request !9546
5
Gemfile
|
@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0'
|
|||
gem 'mysql2', '~> 0.4.5', group: :mysql
|
||||
gem 'pg', '~> 0.18.2', group: :postgres
|
||||
|
||||
gem 'rugged', '~> 0.25.1.1'
|
||||
gem 'rugged', '~> 0.26.0'
|
||||
gem 'grape-route-helpers', '~> 2.0.0'
|
||||
|
||||
gem 'faraday', '~> 0.12'
|
||||
|
@ -58,6 +58,9 @@ gem 'validates_hostname', '~> 1.0.6'
|
|||
# Browser detection
|
||||
gem 'browser', '~> 2.2'
|
||||
|
||||
# GPG
|
||||
gem 'gpgme'
|
||||
|
||||
# LDAP Auth
|
||||
# GitLab fork with several improvements to original library. For full list of changes
|
||||
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
|
||||
|
|
|
@ -332,6 +332,8 @@ GEM
|
|||
multi_json (~> 1.11)
|
||||
os (~> 0.9)
|
||||
signet (~> 0.7)
|
||||
gpgme (2.0.13)
|
||||
mini_portile2 (~> 2.1)
|
||||
grape (0.19.2)
|
||||
activesupport
|
||||
builder
|
||||
|
@ -749,7 +751,7 @@ GEM
|
|||
rubyzip (1.2.1)
|
||||
rufus-scheduler (3.4.0)
|
||||
et-orbi (~> 1.0)
|
||||
rugged (0.25.1.1)
|
||||
rugged (0.26.0)
|
||||
safe_yaml (1.0.4)
|
||||
sanitize (2.1.0)
|
||||
nokogiri (>= 1.4.4)
|
||||
|
@ -983,6 +985,7 @@ DEPENDENCIES
|
|||
gollum-rugged_adapter (~> 0.4.4)
|
||||
gon (~> 6.1.0)
|
||||
google-api-client (~> 0.8.6)
|
||||
gpgme
|
||||
grape (~> 0.19.2)
|
||||
grape-entity (~> 0.6.0)
|
||||
grape-route-helpers (~> 2.0.0)
|
||||
|
@ -1081,7 +1084,7 @@ DEPENDENCIES
|
|||
ruby-prof (~> 0.16.2)
|
||||
ruby_parser (~> 3.8)
|
||||
rufus-scheduler (~> 3.4)
|
||||
rugged (~> 0.25.1.1)
|
||||
rugged (~> 0.26.0)
|
||||
sanitize (~> 2.0)
|
||||
sass-rails (~> 5.0.6)
|
||||
scss_lint (~> 0.54.0)
|
||||
|
|
1
app/assets/javascripts/commons/bootstrap.js
vendored
|
@ -8,6 +8,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
|
|||
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
|
||||
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
|
||||
|
||||
// custom jQuery functions
|
||||
$.fn.extend({
|
||||
|
|
|
@ -64,6 +64,7 @@ import initSettingsPanels from './settings_panels';
|
|||
import initExperimentalFlags from './experimental_flags';
|
||||
import OAuthRememberMe from './oauth_remember_me';
|
||||
import PerformanceBar from './performance_bar';
|
||||
import GpgBadges from './gpg_badges';
|
||||
|
||||
(function() {
|
||||
var Dispatcher;
|
||||
|
@ -300,6 +301,9 @@ import PerformanceBar from './performance_bar';
|
|||
}).bindEvents();
|
||||
break;
|
||||
case 'projects:commits:show':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
GpgBadges.fetch();
|
||||
break;
|
||||
case 'projects:activity':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
break;
|
||||
|
|
15
app/assets/javascripts/gpg_badges.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
export default class GpgBadges {
|
||||
static fetch() {
|
||||
const form = $('.commits-search-form');
|
||||
|
||||
$.get({
|
||||
url: form.data('signatures-path'),
|
||||
data: form.serialize(),
|
||||
}).done((response) => {
|
||||
const badges = $('.js-loading-gpg-badge');
|
||||
response.signatures.forEach((signature) => {
|
||||
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -159,6 +159,8 @@ document.addEventListener('beforeunload', function () {
|
|||
$(document).off('scroll');
|
||||
// Close any open tooltips
|
||||
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
|
||||
// Close any open popover
|
||||
$('[data-toggle="popover"]').popover('destroy');
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', gl.utils.handleLocationHash);
|
||||
|
@ -247,6 +249,11 @@ $(function () {
|
|||
return $(el).data('placement') || 'bottom';
|
||||
}
|
||||
});
|
||||
// Initialize popovers
|
||||
$body.popover({
|
||||
selector: '[data-toggle="popover"]',
|
||||
trigger: 'focus'
|
||||
});
|
||||
$('.trigger-submit').on('change', function () {
|
||||
return $(this).parents('form').submit();
|
||||
// Form submitter
|
||||
|
|
|
@ -118,3 +118,29 @@
|
|||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Mixin for status badges, as used for pipelines and commit signatures
|
||||
*/
|
||||
@mixin status-color($color-light, $color-main, $color-dark) {
|
||||
color: $color-main;
|
||||
border-color: $color-main;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: $color-light;
|
||||
color: $color-dark;
|
||||
border-color: $color-dark;
|
||||
|
||||
svg {
|
||||
fill: $color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $color-main;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin green-status-color {
|
||||
@include status-color($green-50, $green-500, $green-700);
|
||||
}
|
||||
|
|
|
@ -283,3 +283,63 @@
|
|||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.gpg-status-box {
|
||||
&.valid {
|
||||
@include green-status-color;
|
||||
}
|
||||
|
||||
&.invalid {
|
||||
@include status-color($gray-dark, $gray, $common-gray-dark);
|
||||
border-color: $common-gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.gpg-popover-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.gpg-popover-icon {
|
||||
// same margin as .s32.avatar
|
||||
margin-right: $btn-side-margin;
|
||||
|
||||
&.valid {
|
||||
svg {
|
||||
border: 1px solid $brand-success;
|
||||
|
||||
fill: $brand-success;
|
||||
}
|
||||
}
|
||||
|
||||
&.invalid {
|
||||
svg {
|
||||
border: 1px solid $common-gray-light;
|
||||
|
||||
fill: $common-gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.gpg-popover-user-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $gl-padding / 2;
|
||||
text-decoration: none;
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.commit .gpg-popover-help-link {
|
||||
display: block;
|
||||
color: $link-color;
|
||||
}
|
||||
|
|
|
@ -391,3 +391,26 @@ table.u2f-registrations {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.gpg-email-badge {
|
||||
display: inline;
|
||||
margin-right: $gl-padding / 2;
|
||||
|
||||
.gpg-email-badge-email {
|
||||
display: inline;
|
||||
margin-right: $gl-padding / 4;
|
||||
}
|
||||
|
||||
.label-verification-status {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
|
||||
&.verified {
|
||||
@include green-status-color;
|
||||
}
|
||||
|
||||
&.unverified {
|
||||
@include status-color($gray-dark, $gray, $common-gray-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,3 @@
|
|||
@mixin status-color($color-light, $color-main, $color-dark) {
|
||||
color: $color-main;
|
||||
border-color: $color-main;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: $color-light;
|
||||
color: $color-dark;
|
||||
border-color: $color-dark;
|
||||
|
||||
svg {
|
||||
fill: $color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $color-main;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status {
|
||||
padding: 2px 7px 4px;
|
||||
border: 1px solid $gray-darker;
|
||||
|
@ -41,7 +22,7 @@
|
|||
}
|
||||
|
||||
&.ci-success {
|
||||
@include status-color($green-50, $green-500, $green-700);
|
||||
@include green-status-color;
|
||||
}
|
||||
|
||||
&.ci-canceled,
|
||||
|
|
47
app/controllers/profiles/gpg_keys_controller.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
class Profiles::GpgKeysController < Profiles::ApplicationController
|
||||
before_action :set_gpg_key, only: [:destroy, :revoke]
|
||||
|
||||
def index
|
||||
@gpg_keys = current_user.gpg_keys
|
||||
@gpg_key = GpgKey.new
|
||||
end
|
||||
|
||||
def create
|
||||
@gpg_key = current_user.gpg_keys.new(gpg_key_params)
|
||||
|
||||
if @gpg_key.save
|
||||
redirect_to profile_gpg_keys_path
|
||||
else
|
||||
@gpg_keys = current_user.gpg_keys.select(&:persisted?)
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@gpg_key.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to profile_gpg_keys_url, status: 302 }
|
||||
format.js { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
def revoke
|
||||
@gpg_key.revoke
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to profile_gpg_keys_url, status: 302 }
|
||||
format.js { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gpg_key_params
|
||||
params.require(:gpg_key).permit(:key)
|
||||
end
|
||||
|
||||
def set_gpg_key
|
||||
@gpg_key = current_user.gpg_keys.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -6,18 +6,9 @@ class Projects::CommitsController < Projects::ApplicationController
|
|||
before_action :require_non_empty_project
|
||||
before_action :assign_ref_vars
|
||||
before_action :authorize_download_code!
|
||||
before_action :set_commits
|
||||
|
||||
def show
|
||||
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
|
||||
search = params[:search]
|
||||
|
||||
@commits =
|
||||
if search.present?
|
||||
@repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
|
||||
else
|
||||
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
|
||||
end
|
||||
|
||||
@note_counts = project.notes.where(commit_id: @commits.map(&:id))
|
||||
.group(:commit_id).count
|
||||
|
||||
|
@ -37,4 +28,33 @@ class Projects::CommitsController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def signatures
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
signatures: @commits.select(&:has_signature?).map do |commit|
|
||||
{
|
||||
commit_sha: commit.sha,
|
||||
html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_commits
|
||||
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
|
||||
search = params[:search]
|
||||
|
||||
@commits =
|
||||
if search.present?
|
||||
@repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
|
||||
else
|
||||
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -113,6 +113,10 @@ module CommitsHelper
|
|||
commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
|
||||
end
|
||||
|
||||
def commit_signature_badge_classes(additional_classes)
|
||||
%w(btn status-box gpg-status-box) + Array(additional_classes)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Private: Returns a link to a person. If the person has a matching user and
|
||||
|
|
|
@ -14,7 +14,7 @@ module Emails
|
|||
end
|
||||
|
||||
def new_ssh_key_email(key_id)
|
||||
@key = Key.find_by_id(key_id)
|
||||
@key = Key.find_by(id: key_id)
|
||||
|
||||
return unless @key
|
||||
|
||||
|
@ -22,5 +22,15 @@ module Emails
|
|||
@target_url = user_url(@user)
|
||||
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
|
||||
end
|
||||
|
||||
def new_gpg_key_email(gpg_key_id)
|
||||
@gpg_key = GpgKey.find_by(id: gpg_key_id)
|
||||
|
||||
return unless @gpg_key
|
||||
|
||||
@current_user = @user = @gpg_key.user
|
||||
@target_url = user_url(@user)
|
||||
mail(to: @user.notification_email, subject: subject("GPG key was added to your account"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -234,6 +234,14 @@ class Commit
|
|||
@statuses[ref] = pipelines.latest_status(ref)
|
||||
end
|
||||
|
||||
def signature
|
||||
return @signature if defined?(@signature)
|
||||
|
||||
@signature = gpg_commit.signature
|
||||
end
|
||||
|
||||
delegate :has_signature?, to: :gpg_commit
|
||||
|
||||
def revert_branch_name
|
||||
"revert-#{short_id}"
|
||||
end
|
||||
|
@ -382,4 +390,8 @@ class Commit
|
|||
def merged_merge_request_no_cache(user)
|
||||
MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
|
||||
end
|
||||
|
||||
def gpg_commit
|
||||
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
|
||||
end
|
||||
end
|
||||
|
|
107
app/models/gpg_key.rb
Normal file
|
@ -0,0 +1,107 @@
|
|||
class GpgKey < ActiveRecord::Base
|
||||
KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze
|
||||
KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----'.freeze
|
||||
|
||||
include ShaAttribute
|
||||
|
||||
sha_attribute :primary_keyid
|
||||
sha_attribute :fingerprint
|
||||
|
||||
belongs_to :user
|
||||
has_many :gpg_signatures
|
||||
|
||||
validates :user, presence: true
|
||||
|
||||
validates :key,
|
||||
presence: true,
|
||||
uniqueness: true,
|
||||
format: {
|
||||
with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX})(?!#{KEY_SUFFIX}).)+#{KEY_SUFFIX}\Z/m,
|
||||
message: "is invalid. A valid public GPG key begins with '#{KEY_PREFIX}' and ends with '#{KEY_SUFFIX}'"
|
||||
}
|
||||
|
||||
validates :fingerprint,
|
||||
presence: true,
|
||||
uniqueness: true,
|
||||
# only validate when the `key` is valid, as we don't want the user to show
|
||||
# the error about the fingerprint
|
||||
unless: -> { errors.has_key?(:key) }
|
||||
|
||||
validates :primary_keyid,
|
||||
presence: true,
|
||||
uniqueness: true,
|
||||
# only validate when the `key` is valid, as we don't want the user to show
|
||||
# the error about the fingerprint
|
||||
unless: -> { errors.has_key?(:key) }
|
||||
|
||||
before_validation :extract_fingerprint, :extract_primary_keyid
|
||||
after_commit :update_invalid_gpg_signatures, on: :create
|
||||
after_commit :notify_user, on: :create
|
||||
|
||||
def primary_keyid
|
||||
super&.upcase
|
||||
end
|
||||
|
||||
def fingerprint
|
||||
super&.upcase
|
||||
end
|
||||
|
||||
def key=(value)
|
||||
super(value&.strip)
|
||||
end
|
||||
|
||||
def user_infos
|
||||
@user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
|
||||
end
|
||||
|
||||
def verified_user_infos
|
||||
user_infos.select do |user_info|
|
||||
user_info[:email] == user.email
|
||||
end
|
||||
end
|
||||
|
||||
def emails_with_verified_status
|
||||
user_infos.map do |user_info|
|
||||
[
|
||||
user_info[:email],
|
||||
user_info[:email] == user.email
|
||||
]
|
||||
end.to_h
|
||||
end
|
||||
|
||||
def verified?
|
||||
emails_with_verified_status.any? { |_email, verified| verified }
|
||||
end
|
||||
|
||||
def update_invalid_gpg_signatures
|
||||
InvalidGpgSignatureUpdateWorker.perform_async(self.id)
|
||||
end
|
||||
|
||||
def revoke
|
||||
GpgSignature.where(gpg_key: self, valid_signature: true).update_all(
|
||||
gpg_key_id: nil,
|
||||
valid_signature: false,
|
||||
updated_at: Time.zone.now
|
||||
)
|
||||
|
||||
destroy
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_fingerprint
|
||||
# we can assume that the result only contains one item as the validation
|
||||
# only allows one key
|
||||
self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first
|
||||
end
|
||||
|
||||
def extract_primary_keyid
|
||||
# we can assume that the result only contains one item as the validation
|
||||
# only allows one key
|
||||
self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
|
||||
end
|
||||
|
||||
def notify_user
|
||||
NotificationService.new.new_gpg_key(self)
|
||||
end
|
||||
end
|
21
app/models/gpg_signature.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
class GpgSignature < ActiveRecord::Base
|
||||
include ShaAttribute
|
||||
|
||||
sha_attribute :commit_sha
|
||||
sha_attribute :gpg_key_primary_keyid
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :gpg_key
|
||||
|
||||
validates :commit_sha, presence: true
|
||||
validates :project_id, presence: true
|
||||
validates :gpg_key_primary_keyid, presence: true
|
||||
|
||||
def gpg_key_primary_keyid
|
||||
super&.upcase
|
||||
end
|
||||
|
||||
def commit
|
||||
project.commit(commit_sha)
|
||||
end
|
||||
end
|
|
@ -76,6 +76,7 @@ class User < ActiveRecord::Base
|
|||
where(type.not_eq('DeployKey').or(type.eq(nil)))
|
||||
end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :gpg_keys
|
||||
|
||||
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
@ -157,6 +158,7 @@ class User < ActiveRecord::Base
|
|||
before_save :ensure_authentication_token, :ensure_incoming_email_token
|
||||
before_save :ensure_user_rights_and_limits, if: :external_changed?
|
||||
after_save :ensure_namespace_correct
|
||||
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
|
||||
after_initialize :set_projects_limit
|
||||
after_destroy :post_destroy_hook
|
||||
|
||||
|
@ -512,6 +514,10 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def update_invalid_gpg_signatures
|
||||
gpg_keys.each(&:update_invalid_gpg_signatures)
|
||||
end
|
||||
|
||||
# Returns the groups a user has access to
|
||||
def authorized_groups
|
||||
union = Gitlab::SQL::Union
|
||||
|
|
|
@ -56,6 +56,8 @@ class GitPushService < BaseService
|
|||
perform_housekeeping
|
||||
|
||||
update_caches
|
||||
|
||||
update_signatures
|
||||
end
|
||||
|
||||
def update_gitattributes
|
||||
|
@ -80,6 +82,12 @@ class GitPushService < BaseService
|
|||
ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size])
|
||||
end
|
||||
|
||||
def update_signatures
|
||||
@push_commits.each do |commit|
|
||||
CreateGpgSignatureWorker.perform_async(commit.sha, @project.id)
|
||||
end
|
||||
end
|
||||
|
||||
# Schedules processing of commit messages.
|
||||
def process_commit_messages
|
||||
default = is_default_branch?
|
||||
|
|
|
@ -17,6 +17,16 @@ class NotificationService
|
|||
end
|
||||
end
|
||||
|
||||
# Always notify the user about gpg key added
|
||||
#
|
||||
# This is a security email so it will be sent even if the user user disabled
|
||||
# notifications
|
||||
def new_gpg_key(gpg_key)
|
||||
if gpg_key.user
|
||||
mailer.new_gpg_key_email(gpg_key.id).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
# Always notify user about email added to profile
|
||||
def new_email(email)
|
||||
if email.user
|
||||
|
|
|
@ -47,6 +47,10 @@
|
|||
= link_to profile_keys_path, title: 'SSH Keys' do
|
||||
%span
|
||||
SSH Keys
|
||||
= nav_link(controller: :gpg_keys) do
|
||||
= link_to profile_gpg_keys_path, title: 'GPG Keys' do
|
||||
%span
|
||||
GPG Keys
|
||||
= nav_link(controller: :preferences) do
|
||||
= link_to profile_preferences_path, title: 'Preferences' do
|
||||
%span
|
||||
|
|
|
@ -43,6 +43,10 @@
|
|||
= link_to profile_keys_path, title: 'SSH Keys' do
|
||||
%span
|
||||
SSH Keys
|
||||
= nav_link(controller: :gpg_keys) do
|
||||
= link_to profile_gpg_keys_path, title: 'GPG Keys' do
|
||||
%span
|
||||
GPG Keys
|
||||
= nav_link(controller: :preferences) do
|
||||
= link_to profile_preferences_path, title: 'Preferences' do
|
||||
%span
|
||||
|
|
10
app/views/notify/new_gpg_key_email.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
%p
|
||||
Hi #{@user.name}!
|
||||
%p
|
||||
A new GPG key was added to your account:
|
||||
%p
|
||||
Fingerprint:
|
||||
%code= @gpg_key.fingerprint
|
||||
%p
|
||||
If this key was added in error, you can remove it under
|
||||
= link_to "GPG Keys", profile_gpg_keys_url
|
7
app/views/notify/new_gpg_key_email.text.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
Hi <%= @user.name %>!
|
||||
|
||||
A new GPG key was added to your account:
|
||||
|
||||
Fingerprint: <%= @gpg_key.fingerprint %>
|
||||
|
||||
If this key was added in error, you can remove it at <%= profile_gpg_keys_url %>
|
8
app/views/profiles/gpg_keys/_email_with_badge.html.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
- css_classes = %w(label label-verification-status)
|
||||
- css_classes << (verified ? 'verified': 'unverified')
|
||||
- text = verified ? 'Verified' : 'Unverified'
|
||||
|
||||
.gpg-email-badge
|
||||
.gpg-email-badge-email= email
|
||||
%div{ class: css_classes }
|
||||
= text
|
10
app/views/profiles/gpg_keys/_form.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
%div
|
||||
= form_for [:profile, @gpg_key], html: { class: 'js-requires-input' } do |f|
|
||||
= form_errors(@gpg_key)
|
||||
|
||||
.form-group
|
||||
= f.label :key, class: 'label-light'
|
||||
= f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
|
||||
|
||||
.prepend-top-default
|
||||
= f.submit 'Add key', class: "btn btn-create"
|
18
app/views/profiles/gpg_keys/_key.html.haml
Normal file
|
@ -0,0 +1,18 @@
|
|||
%li.key-list-item
|
||||
.pull-left.append-right-10
|
||||
= icon 'key', class: "settings-list-icon hidden-xs"
|
||||
.key-list-item-info
|
||||
- key.emails_with_verified_status.map do |email, verified|
|
||||
= render partial: 'email_with_badge', locals: { email: email, verified: verified }
|
||||
|
||||
.description
|
||||
%code= key.fingerprint
|
||||
.pull-right
|
||||
%span.key-created-at
|
||||
created #{time_ago_with_tooltip(key.created_at)}
|
||||
= link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
|
||||
%span.sr-only Remove
|
||||
= icon('trash')
|
||||
= link_to revoke_profile_gpg_key_path(key), data: { confirm: 'Are you sure? All commits that were signed with this GPG key will be unverified.' }, method: :put, class: "btn btn-danger prepend-left-10" do
|
||||
%span.sr-only Revoke
|
||||
Revoke
|
11
app/views/profiles/gpg_keys/_key_table.html.haml
Normal file
|
@ -0,0 +1,11 @@
|
|||
- is_admin = local_assigns.fetch(:admin, false)
|
||||
|
||||
- if @gpg_keys.any?
|
||||
%ul.well-list
|
||||
= render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin }
|
||||
- else
|
||||
%p.settings-message.text-center
|
||||
- if is_admin
|
||||
There are no GPG keys associated with this account.
|
||||
- else
|
||||
There are no GPG keys with access to your account.
|
21
app/views/profiles/gpg_keys/index.html.haml
Normal file
|
@ -0,0 +1,21 @@
|
|||
- page_title "GPG Keys"
|
||||
= render 'profiles/head'
|
||||
|
||||
.row.prepend-top-default
|
||||
.col-lg-3.profile-settings-sidebar
|
||||
%h4.prepend-top-0
|
||||
= page_title
|
||||
%p
|
||||
GPG keys allow you to verify signed commits.
|
||||
.col-lg-9
|
||||
%h5.prepend-top-0
|
||||
Add a GPG key
|
||||
%p.profile-settings-content
|
||||
Before you can add a GPG key you need to
|
||||
= link_to 'generate it.', help_page_path('workflow/gpg_signed_commits/index.md')
|
||||
= render 'form'
|
||||
%hr
|
||||
%h5
|
||||
Your GPG keys (#{@gpg_keys.count})
|
||||
.append-bottom-default
|
||||
= render 'key_table'
|
3
app/views/projects/commit/_ajax_signature.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
- if commit.has_signature?
|
||||
%button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
|
||||
%i.fa.fa-spinner.fa-spin
|
|
@ -1,5 +1,6 @@
|
|||
.page-content-header
|
||||
.header-main-content
|
||||
= render partial: 'signature', object: @commit.signature
|
||||
%strong
|
||||
#{ s_('CommitBoxTitle|Commit') }
|
||||
%span.commit-sha= @commit.short_id
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
- title = capture do
|
||||
.gpg-popover-icon.invalid
|
||||
= render 'shared/icons/icon_status_notfound_borderless.svg'
|
||||
%div
|
||||
This commit was signed with an <strong>unverified</strong> signature.
|
||||
|
||||
- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] }
|
||||
|
||||
= render partial: 'projects/commit/signature_badge', locals: locals
|
5
app/views/projects/commit/_signature.html.haml
Normal file
|
@ -0,0 +1,5 @@
|
|||
- if signature
|
||||
- if signature.valid_signature?
|
||||
= render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
|
||||
- else
|
||||
= render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
|
18
app/views/projects/commit/_signature_badge.html.haml
Normal file
|
@ -0,0 +1,18 @@
|
|||
- css_classes = commit_signature_badge_classes(css_classes)
|
||||
|
||||
- title = capture do
|
||||
.gpg-popover-status
|
||||
= title
|
||||
|
||||
- content = capture do
|
||||
.clearfix
|
||||
= content
|
||||
|
||||
GPG Key ID:
|
||||
%span.monospace= signature.gpg_key_primary_keyid
|
||||
|
||||
|
||||
= link_to('Learn more about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
|
||||
|
||||
%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
|
||||
= label
|
32
app/views/projects/commit/_valid_signature_badge.html.haml
Normal file
|
@ -0,0 +1,32 @@
|
|||
- title = capture do
|
||||
.gpg-popover-icon.valid
|
||||
= render 'shared/icons/icon_status_success_borderless.svg'
|
||||
%div
|
||||
This commit was signed with a <strong>verified</strong> signature.
|
||||
|
||||
- content = capture do
|
||||
- gpg_key = signature.gpg_key
|
||||
- user = gpg_key&.user
|
||||
- user_name = signature.gpg_key_user_name
|
||||
- user_email = signature.gpg_key_user_email
|
||||
|
||||
- if user
|
||||
= link_to user_path(user), class: 'gpg-popover-user-link' do
|
||||
%div
|
||||
= user_avatar_without_link(user: user, size: 32)
|
||||
|
||||
%div
|
||||
%strong= gpg_key.user.name
|
||||
%div @#{gpg_key.user.username}
|
||||
- else
|
||||
= mail_to user_email do
|
||||
%div
|
||||
= user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
|
||||
|
||||
%div
|
||||
%strong= user_name
|
||||
%div= user_email
|
||||
|
||||
- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] }
|
||||
|
||||
= render partial: 'projects/commit/signature_badge', locals: locals
|
|
@ -9,7 +9,7 @@
|
|||
- cache_key.push(commit.status(ref)) if commit.status(ref)
|
||||
|
||||
= cache(cache_key, expires_in: 1.day) do
|
||||
%li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
|
||||
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
|
||||
|
||||
.avatar-cell.hidden-xs
|
||||
= author_avatar(commit, size: 36)
|
||||
|
@ -36,9 +36,15 @@
|
|||
#{ commit_text.html_safe }
|
||||
|
||||
|
||||
.commit-actions.flex-row.hidden-xs
|
||||
.commit-actions.hidden-xs
|
||||
- if commit.status(ref)
|
||||
= render_commit_status(commit, ref: ref)
|
||||
|
||||
- if request.xhr?
|
||||
= render partial: 'projects/commit/signature', object: commit.signature
|
||||
- else
|
||||
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
|
||||
|
||||
= link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent"
|
||||
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
|
||||
= link_to_browse_code(project, commit)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
%span.commits-count= n_("%d commit", "%d commits", commits.count) % commits.count
|
||||
|
||||
%li.commits-row{ data: { day: day } }
|
||||
%ul.content-list.commit-list
|
||||
%ul.content-list.commit-list.flex-list
|
||||
= render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref }
|
||||
|
||||
- if hidden > 0
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
|
||||
|
||||
.control
|
||||
= form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form') do
|
||||
= form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form', data: { 'signatures-path' => namespace_project_signatures_path }) do
|
||||
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
|
||||
.control
|
||||
= link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0V11.78a5.9 5.9 0 0 0 .827-.492z" fill-rule="nonzero"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></svg>
|
After Width: | Height: | Size: 561 B |
16
app/workers/create_gpg_signature_worker.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class CreateGpgSignatureWorker
|
||||
include Sidekiq::Worker
|
||||
include DedicatedSidekiqQueue
|
||||
|
||||
def perform(commit_sha, project_id)
|
||||
project = Project.find_by(id: project_id)
|
||||
|
||||
return unless project
|
||||
|
||||
commit = project.commit(commit_sha)
|
||||
|
||||
return unless commit
|
||||
|
||||
commit.signature
|
||||
end
|
||||
end
|
12
app/workers/invalid_gpg_signature_update_worker.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class InvalidGpgSignatureUpdateWorker
|
||||
include Sidekiq::Worker
|
||||
include DedicatedSidekiqQueue
|
||||
|
||||
def perform(gpg_key_id)
|
||||
gpg_key = GpgKey.find_by(id: gpg_key_id)
|
||||
|
||||
return unless gpg_key
|
||||
|
||||
Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run
|
||||
end
|
||||
end
|
4
changelogs/unreleased/feature-gpg-signed-commits.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: GPG signed commits integration
|
||||
merge_request: 9546
|
||||
author: Alexis Reigel
|
21
config/initializers/mysql_set_length_for_binary_indexes.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# This patches ActiveRecord so indexes for binary columns created using the
|
||||
# MySQL adapter apply a length of 20. Otherwise MySQL can't create an index on
|
||||
# binary columns.
|
||||
|
||||
module MysqlSetLengthForBinaryIndex
|
||||
def add_index(table_name, column_names, options = {})
|
||||
Array(column_names).each do |column_name|
|
||||
column = ActiveRecord::Base.connection.columns(table_name).find { |c| c.name == column_name }
|
||||
|
||||
if column&.type == :binary
|
||||
options[:length] = 20
|
||||
end
|
||||
end
|
||||
|
||||
super(table_name, column_names, options)
|
||||
end
|
||||
end
|
||||
|
||||
if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
||||
ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:prepend, MysqlSetLengthForBinaryIndex)
|
||||
end
|
|
@ -23,6 +23,11 @@ resource :profile, only: [:show, :update] do
|
|||
end
|
||||
resource :preferences, only: [:show, :update]
|
||||
resources :keys, only: [:index, :show, :create, :destroy]
|
||||
resources :gpg_keys, only: [:index, :create, :destroy] do
|
||||
member do
|
||||
put :revoke
|
||||
end
|
||||
end
|
||||
resources :emails, only: [:index, :create, :destroy]
|
||||
resources :chat_names, only: [:index, :new, :create, :destroy] do
|
||||
collection do
|
||||
|
|
|
@ -76,6 +76,8 @@ scope format: false do
|
|||
get '/tree/*id', to: 'tree#show', as: :tree
|
||||
get '/raw/*id', to: 'raw#show', as: :raw
|
||||
get '/blame/*id', to: 'blame#show', as: :blame
|
||||
|
||||
get '/commits/*id/signatures', to: 'commits#signatures', as: :signatures
|
||||
get '/commits/*id', to: 'commits#show', as: :commits
|
||||
|
||||
post '/create_dir/*id', to: 'tree#create_dir', as: :create_dir
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
- [email_receiver, 2]
|
||||
- [emails_on_push, 2]
|
||||
- [mailers, 2]
|
||||
- [invalid_gpg_signature_update, 2]
|
||||
- [create_gpg_signature, 2]
|
||||
- [upload_checksum, 1]
|
||||
- [use_key, 1]
|
||||
- [repository_fork, 1]
|
||||
|
|
19
db/migrate/20170222111732_create_gpg_keys.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
class CreateGpgKeys < ActiveRecord::Migration
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :gpg_keys do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
|
||||
t.references :user, index: true, foreign_key: { on_delete: :cascade }
|
||||
|
||||
t.binary :primary_keyid
|
||||
t.binary :fingerprint
|
||||
|
||||
t.text :key
|
||||
|
||||
t.index :primary_keyid, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
|
||||
t.index :fingerprint, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
|
||||
end
|
||||
end
|
||||
end
|
23
db/migrate/20170613154149_create_gpg_signatures.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
class CreateGpgSignatures < ActiveRecord::Migration
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :gpg_signatures do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
|
||||
t.references :project, index: true, foreign_key: { on_delete: :cascade }
|
||||
t.references :gpg_key, index: true, foreign_key: { on_delete: :nullify }
|
||||
|
||||
t.boolean :valid_signature
|
||||
|
||||
t.binary :commit_sha
|
||||
t.binary :gpg_key_primary_keyid
|
||||
|
||||
t.text :gpg_key_user_name
|
||||
t.text :gpg_key_user_email
|
||||
|
||||
t.index :commit_sha, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
|
||||
t.index :gpg_key_primary_keyid, length: Gitlab::Database.mysql? ? 20 : nil
|
||||
end
|
||||
end
|
||||
end
|
33
db/schema.rb
|
@ -540,6 +540,36 @@ ActiveRecord::Schema.define(version: 20170725145659) do
|
|||
|
||||
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
|
||||
|
||||
create_table "gpg_keys", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "user_id"
|
||||
t.binary "primary_keyid"
|
||||
t.binary "fingerprint"
|
||||
t.text "key"
|
||||
end
|
||||
|
||||
add_index "gpg_keys", ["fingerprint"], name: "index_gpg_keys_on_fingerprint", unique: true, using: :btree
|
||||
add_index "gpg_keys", ["primary_keyid"], name: "index_gpg_keys_on_primary_keyid", unique: true, using: :btree
|
||||
add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree
|
||||
|
||||
create_table "gpg_signatures", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "project_id"
|
||||
t.integer "gpg_key_id"
|
||||
t.boolean "valid_signature"
|
||||
t.binary "commit_sha"
|
||||
t.binary "gpg_key_primary_keyid"
|
||||
t.text "gpg_key_user_name"
|
||||
t.text "gpg_key_user_email"
|
||||
end
|
||||
|
||||
add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
|
||||
add_index "gpg_signatures", ["gpg_key_id"], name: "index_gpg_signatures_on_gpg_key_id", using: :btree
|
||||
add_index "gpg_signatures", ["gpg_key_primary_keyid"], name: "index_gpg_signatures_on_gpg_key_primary_keyid", using: :btree
|
||||
add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
|
||||
|
||||
create_table "identities", force: :cascade do |t|
|
||||
t.string "extern_uid"
|
||||
t.string "provider"
|
||||
|
@ -1602,6 +1632,9 @@ ActiveRecord::Schema.define(version: 20170725145659) do
|
|||
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
|
||||
add_foreign_key "events", "projects", name: "fk_0434b48643", on_delete: :cascade
|
||||
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
|
||||
add_foreign_key "gpg_keys", "users", on_delete: :cascade
|
||||
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
|
||||
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
|
||||
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
|
||||
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
|
||||
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
|
||||
|
|
|
@ -89,6 +89,7 @@ Manage files and branches from the UI (user interface):
|
|||
- [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use.
|
||||
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf): Download a PDF describing the most used Git operations.
|
||||
- [GitLab Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy.
|
||||
- [Signing commits](workflow/gpg_signed_commits/index.md): use GPG to sign your commits.
|
||||
|
||||
### Migrate and import your projects from other platforms
|
||||
|
||||
|
|
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 14 KiB |
84
doc/workflow/gpg_signed_commits/index.md
Normal file
|
@ -0,0 +1,84 @@
|
|||
# Signing commits with GPG
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Git Tools - Signing Your Work](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work)
|
||||
- [Git Tools - Signing Your Work: GPG introduction](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work#_gpg_introduction)
|
||||
- [Git Tools - Signing Your Work: Signing commits](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work#_signing_commits)
|
||||
|
||||
## How GitLab handles GPG
|
||||
|
||||
GitLab uses its own keyring to verify the GPG signature. It does not access any
|
||||
public key server.
|
||||
|
||||
In order to have a commit verified on GitLab the corresponding public key needs
|
||||
to be uploaded to GitLab.
|
||||
|
||||
For a signature to be verified two prerequisites need to be met:
|
||||
|
||||
1. The public key needs to be added to GitLab
|
||||
1. One of the emails in the GPG key matches your **primary** email
|
||||
|
||||
## Add a GPG key
|
||||
|
||||
1. On the upper right corner, click on your avatar and go to your **Settings**.
|
||||
|
||||
![Settings dropdown](../../gitlab-basics/img/profile_settings.png)
|
||||
|
||||
1. Navigate to the **GPG keys** tab.
|
||||
|
||||
![GPG Keys](img/profile_settings_gpg_keys.png)
|
||||
|
||||
1. Paste your **public** key in the 'Key' box.
|
||||
|
||||
![Paste GPG public key](img/profile_settings_gpg_keys_paste_pub.png)
|
||||
|
||||
1. Finally, click on **Add key** to add it to GitLab. You will be able to see
|
||||
its fingerprint, the corresponding email address and creation date.
|
||||
|
||||
![GPG key single page](img/profile_settings_gpg_keys_single_key.png)
|
||||
|
||||
>**Note:**
|
||||
Once you add a key, you cannot edit it, only remove it. In case the paste
|
||||
didn't work, you will have to remove the offending key and re-add it.
|
||||
|
||||
## Remove a GPG key
|
||||
|
||||
1. On the upper right corner, click on your avatar and go to your **Settings**.
|
||||
|
||||
1. Navigate to the **GPG keys** tab.
|
||||
|
||||
1. Click on the trash icon besides the GPG key you want to delete.
|
||||
|
||||
>**Note:**
|
||||
Removing a key **does not unverify** already signed commits. Commits that were
|
||||
verified by using this key will stay verified. Only unpushed commits will stay
|
||||
unverified once you remove this key.
|
||||
|
||||
## Revoke a GPG key
|
||||
|
||||
1. On the upper right corner, click on your avatar and go to your **Settings**.
|
||||
|
||||
1. Navigate to the **GPG keys** tab.
|
||||
|
||||
1. Click on **Revoke** besides the GPG key you want to delete.
|
||||
|
||||
>**Note:**
|
||||
Revoking a key **unverifies** already signed commits. Commits that were
|
||||
verified by using this key will change to an unverified state. Future commits
|
||||
will also stay unverified once you revoke this key. This action should be used
|
||||
in case your key has been compromised.
|
||||
|
||||
## Verifying commits
|
||||
|
||||
1. Within a project navigate to the **Commits** tag. Signed commits will show a
|
||||
badge containing either "Verified" or "Unverified", depending on the
|
||||
verification status of the GPG signature.
|
||||
|
||||
![Signed and unsigned commits](img/project_signed_and_unsigned_commits.png)
|
||||
|
||||
1. By clicking on the GPG badge details of the signature are displayed.
|
||||
|
||||
![Signed commit with verified signature](img/project_signed_commit_verified_signature.png)
|
||||
|
||||
![Signed commit with verified signature](img/project_signed_commit_unverified_signature.png)
|
|
@ -319,6 +319,15 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
# Get the gpg signature of this commit.
|
||||
#
|
||||
# Ex.
|
||||
# commit.signature(repo)
|
||||
#
|
||||
def signature(repo)
|
||||
Rugged::Commit.extract_signature(repo.rugged, sha)
|
||||
end
|
||||
|
||||
def stats
|
||||
Gitlab::Git::CommitStats.new(self)
|
||||
end
|
||||
|
@ -327,7 +336,7 @@ module Gitlab
|
|||
begin
|
||||
raw_commit.to_mbox(options)
|
||||
rescue Rugged::InvalidError => ex
|
||||
if ex.message =~ /Commit \w+ is a merge commit/
|
||||
if ex.message =~ /commit \w+ is a merge commit/i
|
||||
'Patch format is not currently supported for merge commits.'
|
||||
end
|
||||
end
|
||||
|
|
62
lib/gitlab/gpg.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
module Gitlab
|
||||
module Gpg
|
||||
extend self
|
||||
|
||||
module CurrentKeyChain
|
||||
extend self
|
||||
|
||||
def add(key)
|
||||
GPGME::Key.import(key)
|
||||
end
|
||||
|
||||
def fingerprints_from_key(key)
|
||||
import = GPGME::Key.import(key)
|
||||
|
||||
return [] if import.imported == 0
|
||||
|
||||
import.imports.map(&:fingerprint)
|
||||
end
|
||||
end
|
||||
|
||||
def fingerprints_from_key(key)
|
||||
using_tmp_keychain do
|
||||
CurrentKeyChain.fingerprints_from_key(key)
|
||||
end
|
||||
end
|
||||
|
||||
def primary_keyids_from_key(key)
|
||||
using_tmp_keychain do
|
||||
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
|
||||
|
||||
GPGME::Key.find(:public, fingerprints).map { |raw_key| raw_key.primary_subkey.keyid }
|
||||
end
|
||||
end
|
||||
|
||||
def user_infos_from_key(key)
|
||||
using_tmp_keychain do
|
||||
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
|
||||
|
||||
GPGME::Key.find(:public, fingerprints).flat_map do |raw_key|
|
||||
raw_key.uids.map { |uid| { name: uid.name, email: uid.email } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def using_tmp_keychain
|
||||
Dir.mktmpdir do |dir|
|
||||
@original_dirs ||= [GPGME::Engine.dirinfo('homedir')]
|
||||
@original_dirs.push(dir)
|
||||
|
||||
GPGME::Engine.home_dir = dir
|
||||
|
||||
return_value = yield
|
||||
|
||||
@original_dirs.pop
|
||||
|
||||
GPGME::Engine.home_dir = @original_dirs[-1]
|
||||
|
||||
return_value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
85
lib/gitlab/gpg/commit.rb
Normal file
|
@ -0,0 +1,85 @@
|
|||
module Gitlab
|
||||
module Gpg
|
||||
class Commit
|
||||
attr_reader :commit
|
||||
|
||||
def initialize(commit)
|
||||
@commit = commit
|
||||
|
||||
@signature_text, @signed_text = commit.raw.signature(commit.project.repository)
|
||||
end
|
||||
|
||||
def has_signature?
|
||||
!!(@signature_text && @signed_text)
|
||||
end
|
||||
|
||||
def signature
|
||||
return unless has_signature?
|
||||
|
||||
cached_signature = GpgSignature.find_by(commit_sha: commit.sha)
|
||||
return cached_signature if cached_signature.present?
|
||||
|
||||
using_keychain do |gpg_key|
|
||||
create_cached_signature!(gpg_key)
|
||||
end
|
||||
end
|
||||
|
||||
def update_signature!(cached_signature)
|
||||
using_keychain do |gpg_key|
|
||||
cached_signature.update_attributes!(attributes(gpg_key))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def using_keychain
|
||||
Gitlab::Gpg.using_tmp_keychain do
|
||||
# first we need to get the keyid from the signature to query the gpg
|
||||
# key belonging to the keyid.
|
||||
# This way we can add the key to the temporary keychain and extract
|
||||
# the proper signature.
|
||||
gpg_key = GpgKey.find_by(primary_keyid: verified_signature.fingerprint)
|
||||
|
||||
if gpg_key
|
||||
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
|
||||
@verified_signature = nil
|
||||
end
|
||||
|
||||
yield gpg_key
|
||||
end
|
||||
end
|
||||
|
||||
def verified_signature
|
||||
@verified_signature ||= GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature|
|
||||
break verified_signature
|
||||
end
|
||||
end
|
||||
|
||||
def create_cached_signature!(gpg_key)
|
||||
GpgSignature.create!(attributes(gpg_key))
|
||||
end
|
||||
|
||||
def attributes(gpg_key)
|
||||
user_infos = user_infos(gpg_key)
|
||||
|
||||
{
|
||||
commit_sha: commit.sha,
|
||||
project: commit.project,
|
||||
gpg_key: gpg_key,
|
||||
gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint,
|
||||
gpg_key_user_name: user_infos[:name],
|
||||
gpg_key_user_email: user_infos[:email],
|
||||
valid_signature: gpg_signature_valid_signature_value(gpg_key)
|
||||
}
|
||||
end
|
||||
|
||||
def gpg_signature_valid_signature_value(gpg_key)
|
||||
!!(gpg_key && gpg_key.verified? && verified_signature.valid?)
|
||||
end
|
||||
|
||||
def user_infos(gpg_key)
|
||||
gpg_key&.verified_user_infos&.first || gpg_key&.user_infos&.first || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
19
lib/gitlab/gpg/invalid_gpg_signature_updater.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
module Gitlab
|
||||
module Gpg
|
||||
class InvalidGpgSignatureUpdater
|
||||
def initialize(gpg_key)
|
||||
@gpg_key = gpg_key
|
||||
end
|
||||
|
||||
def run
|
||||
GpgSignature
|
||||
.select(:id, :commit_sha, :project_id)
|
||||
.where('gpg_key_id IS NULL OR valid_signature = ?', false)
|
||||
.where(gpg_key_primary_keyid: @gpg_key.primary_keyid)
|
||||
.find_each do |gpg_signature|
|
||||
Gitlab::Gpg::Commit.new(gpg_signature.commit).update_signature!(gpg_signature)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
8
spec/factories/gpg_keys.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
require_relative '../support/gpg_helpers'
|
||||
|
||||
FactoryGirl.define do
|
||||
factory :gpg_key do
|
||||
key GpgHelpers::User1.public_key
|
||||
user
|
||||
end
|
||||
end
|
11
spec/factories/gpg_signature.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
require_relative '../support/gpg_helpers'
|
||||
|
||||
FactoryGirl.define do
|
||||
factory :gpg_signature do
|
||||
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
|
||||
project
|
||||
gpg_key
|
||||
gpg_key_primary_keyid { gpg_key.primary_keyid }
|
||||
valid_signature true
|
||||
end
|
||||
end
|
|
@ -203,4 +203,105 @@ describe 'Commits' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GPG signed commits', :js do
|
||||
it 'changes from unverified to verified when the user changes his email to match the gpg key' do
|
||||
user = create :user, email: 'unrelated.user@example.org'
|
||||
project.team << [user, :master]
|
||||
|
||||
Sidekiq::Testing.inline! do
|
||||
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
|
||||
end
|
||||
|
||||
sign_in(user)
|
||||
|
||||
visit project_commits_path(project, :'signed-commits')
|
||||
|
||||
within '#commits-list' do
|
||||
expect(page).to have_content 'Unverified'
|
||||
expect(page).not_to have_content 'Verified'
|
||||
end
|
||||
|
||||
# user changes his email which makes the gpg key verified
|
||||
Sidekiq::Testing.inline! do
|
||||
user.skip_reconfirmation!
|
||||
user.update_attributes!(email: GpgHelpers::User1.emails.first)
|
||||
end
|
||||
|
||||
visit project_commits_path(project, :'signed-commits')
|
||||
|
||||
within '#commits-list' do
|
||||
expect(page).to have_content 'Unverified'
|
||||
expect(page).to have_content 'Verified'
|
||||
end
|
||||
end
|
||||
|
||||
it 'changes from unverified to verified when the user adds the missing gpg key' do
|
||||
user = create :user, email: GpgHelpers::User1.emails.first
|
||||
project.team << [user, :master]
|
||||
|
||||
sign_in(user)
|
||||
|
||||
visit project_commits_path(project, :'signed-commits')
|
||||
|
||||
within '#commits-list' do
|
||||
expect(page).to have_content 'Unverified'
|
||||
expect(page).not_to have_content 'Verified'
|
||||
end
|
||||
|
||||
# user adds the gpg key which makes the signature valid
|
||||
Sidekiq::Testing.inline! do
|
||||
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
|
||||
end
|
||||
|
||||
visit project_commits_path(project, :'signed-commits')
|
||||
|
||||
within '#commits-list' do
|
||||
expect(page).to have_content 'Unverified'
|
||||
expect(page).to have_content 'Verified'
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows popover badges' do
|
||||
gpg_user = create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard'
|
||||
Sidekiq::Testing.inline! do
|
||||
create :gpg_key, key: GpgHelpers::User1.public_key, user: gpg_user
|
||||
end
|
||||
|
||||
user = create :user
|
||||
project.team << [user, :master]
|
||||
|
||||
sign_in(user)
|
||||
visit project_commits_path(project, :'signed-commits')
|
||||
|
||||
# unverified signature
|
||||
click_on 'Unverified', match: :first
|
||||
within '.popover' do
|
||||
expect(page).to have_content 'This commit was signed with an unverified signature.'
|
||||
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
|
||||
end
|
||||
|
||||
# verified and the gpg user has a gitlab profile
|
||||
click_on 'Verified'
|
||||
within '.popover' do
|
||||
expect(page).to have_content 'This commit was signed with a verified signature.'
|
||||
expect(page).to have_content 'Nannie Bernhard'
|
||||
expect(page).to have_content '@nannie.bernhard'
|
||||
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
|
||||
end
|
||||
|
||||
# verified and the gpg user's profile doesn't exist anymore
|
||||
gpg_user.destroy!
|
||||
|
||||
visit project_commits_path(project, :'signed-commits')
|
||||
|
||||
click_on 'Verified'
|
||||
within '.popover' do
|
||||
expect(page).to have_content 'This commit was signed with a verified signature.'
|
||||
expect(page).to have_content 'Nannie Bernhard'
|
||||
expect(page).to have_content 'nannie.bernhard@example.com'
|
||||
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
58
spec/features/profiles/gpg_keys_spec.rb
Normal file
|
@ -0,0 +1,58 @@
|
|||
require 'rails_helper'
|
||||
|
||||
feature 'Profile > GPG Keys' do
|
||||
let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
|
||||
|
||||
before do
|
||||
login_as(user)
|
||||
end
|
||||
|
||||
describe 'User adds a key' do
|
||||
before do
|
||||
visit profile_gpg_keys_path
|
||||
end
|
||||
|
||||
scenario 'saves the new key' do
|
||||
fill_in('Key', with: GpgHelpers::User2.public_key)
|
||||
click_button('Add key')
|
||||
|
||||
expect(page).to have_content('bette.cartwright@example.com Verified')
|
||||
expect(page).to have_content('bette.cartwright@example.net Unverified')
|
||||
expect(page).to have_content(GpgHelpers::User2.fingerprint)
|
||||
end
|
||||
end
|
||||
|
||||
scenario 'User sees their key' do
|
||||
create(:gpg_key, user: user, key: GpgHelpers::User2.public_key)
|
||||
visit profile_gpg_keys_path
|
||||
|
||||
expect(page).to have_content('bette.cartwright@example.com Verified')
|
||||
expect(page).to have_content('bette.cartwright@example.net Unverified')
|
||||
expect(page).to have_content(GpgHelpers::User2.fingerprint)
|
||||
end
|
||||
|
||||
scenario 'User removes a key via the key index' do
|
||||
create(:gpg_key, user: user, key: GpgHelpers::User2.public_key)
|
||||
visit profile_gpg_keys_path
|
||||
|
||||
click_link('Remove')
|
||||
|
||||
expect(page).to have_content('Your GPG keys (0)')
|
||||
end
|
||||
|
||||
scenario 'User revokes a key via the key index' do
|
||||
gpg_key = create :gpg_key, user: user, key: GpgHelpers::User2.public_key
|
||||
gpg_signature = create :gpg_signature, gpg_key: gpg_key, valid_signature: true
|
||||
|
||||
visit profile_gpg_keys_path
|
||||
|
||||
click_link('Revoke')
|
||||
|
||||
expect(page).to have_content('Your GPG keys (0)')
|
||||
|
||||
expect(gpg_signature.reload).to have_attributes(
|
||||
valid_signature: false,
|
||||
gpg_key: nil
|
||||
)
|
||||
end
|
||||
end
|
127
spec/lib/gitlab/gpg/commit_spec.rb
Normal file
|
@ -0,0 +1,127 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Gitlab::Gpg::Commit do
|
||||
describe '#signature' do
|
||||
let!(:project) { create :project, :repository, path: 'sample-project' }
|
||||
let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
|
||||
|
||||
context 'unisgned commit' do
|
||||
it 'returns nil' do
|
||||
expect(described_class.new(project.commit).signature).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'known and verified public key' do
|
||||
let!(:gpg_key) do
|
||||
create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first)
|
||||
end
|
||||
|
||||
let!(:commit) do
|
||||
raw_commit = double(:raw_commit, signature: [
|
||||
GpgHelpers::User1.signed_commit_signature,
|
||||
GpgHelpers::User1.signed_commit_base_data
|
||||
], sha: commit_sha)
|
||||
allow(raw_commit).to receive :save!
|
||||
|
||||
create :commit, git_commit: raw_commit, project: project
|
||||
end
|
||||
|
||||
it 'returns a valid signature' do
|
||||
expect(described_class.new(commit).signature).to have_attributes(
|
||||
commit_sha: commit_sha,
|
||||
project: project,
|
||||
gpg_key: gpg_key,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
gpg_key_user_name: GpgHelpers::User1.names.first,
|
||||
gpg_key_user_email: GpgHelpers::User1.emails.first,
|
||||
valid_signature: true
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the cached signature on second call' do
|
||||
gpg_commit = described_class.new(commit)
|
||||
|
||||
expect(gpg_commit).to receive(:using_keychain).and_call_original
|
||||
gpg_commit.signature
|
||||
|
||||
# consecutive call
|
||||
expect(gpg_commit).not_to receive(:using_keychain).and_call_original
|
||||
gpg_commit.signature
|
||||
end
|
||||
end
|
||||
|
||||
context 'known but unverified public key' do
|
||||
let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key }
|
||||
|
||||
let!(:commit) do
|
||||
raw_commit = double(:raw_commit, signature: [
|
||||
GpgHelpers::User1.signed_commit_signature,
|
||||
GpgHelpers::User1.signed_commit_base_data
|
||||
], sha: commit_sha)
|
||||
allow(raw_commit).to receive :save!
|
||||
|
||||
create :commit, git_commit: raw_commit, project: project
|
||||
end
|
||||
|
||||
it 'returns an invalid signature' do
|
||||
expect(described_class.new(commit).signature).to have_attributes(
|
||||
commit_sha: commit_sha,
|
||||
project: project,
|
||||
gpg_key: gpg_key,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
gpg_key_user_name: GpgHelpers::User1.names.first,
|
||||
gpg_key_user_email: GpgHelpers::User1.emails.first,
|
||||
valid_signature: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the cached signature on second call' do
|
||||
gpg_commit = described_class.new(commit)
|
||||
|
||||
expect(gpg_commit).to receive(:using_keychain).and_call_original
|
||||
gpg_commit.signature
|
||||
|
||||
# consecutive call
|
||||
expect(gpg_commit).not_to receive(:using_keychain).and_call_original
|
||||
gpg_commit.signature
|
||||
end
|
||||
end
|
||||
|
||||
context 'unknown public key' do
|
||||
let!(:commit) do
|
||||
raw_commit = double(:raw_commit, signature: [
|
||||
GpgHelpers::User1.signed_commit_signature,
|
||||
GpgHelpers::User1.signed_commit_base_data
|
||||
], sha: commit_sha)
|
||||
allow(raw_commit).to receive :save!
|
||||
|
||||
create :commit,
|
||||
git_commit: raw_commit,
|
||||
project: project
|
||||
end
|
||||
|
||||
it 'returns an invalid signature' do
|
||||
expect(described_class.new(commit).signature).to have_attributes(
|
||||
commit_sha: commit_sha,
|
||||
project: project,
|
||||
gpg_key: nil,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
gpg_key_user_name: nil,
|
||||
gpg_key_user_email: nil,
|
||||
valid_signature: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the cached signature on second call' do
|
||||
gpg_commit = described_class.new(commit)
|
||||
|
||||
expect(gpg_commit).to receive(:using_keychain).and_call_original
|
||||
gpg_commit.signature
|
||||
|
||||
# consecutive call
|
||||
expect(gpg_commit).not_to receive(:using_keychain).and_call_original
|
||||
gpg_commit.signature
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
173
spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
Normal file
|
@ -0,0 +1,173 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
|
||||
describe '#run' do
|
||||
let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
|
||||
let!(:project) { create :project, :repository, path: 'sample-project' }
|
||||
let!(:raw_commit) do
|
||||
raw_commit = double(:raw_commit, signature: [
|
||||
GpgHelpers::User1.signed_commit_signature,
|
||||
GpgHelpers::User1.signed_commit_base_data
|
||||
], sha: commit_sha)
|
||||
|
||||
allow(raw_commit).to receive :save!
|
||||
|
||||
raw_commit
|
||||
end
|
||||
|
||||
let!(:commit) do
|
||||
create :commit, git_commit: raw_commit, project: project
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Project).to receive(:commit).and_return(commit)
|
||||
end
|
||||
|
||||
context 'gpg signature did have an associated gpg key which was removed later' do
|
||||
let!(:user) { create :user, email: GpgHelpers::User1.emails.first }
|
||||
|
||||
let!(:valid_gpg_signature) do
|
||||
create :gpg_signature,
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: nil,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: true
|
||||
end
|
||||
|
||||
it 'assigns the gpg key to the signature when the missing gpg key is added' do
|
||||
# InvalidGpgSignatureUpdater is called by the after_create hook
|
||||
gpg_key = create :gpg_key,
|
||||
key: GpgHelpers::User1.public_key,
|
||||
user: user
|
||||
|
||||
expect(valid_gpg_signature.reload).to have_attributes(
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: gpg_key,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: true
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not assign the gpg key when an unrelated gpg key is added' do
|
||||
# InvalidGpgSignatureUpdater is called by the after_create hook
|
||||
create :gpg_key,
|
||||
key: GpgHelpers::User2.public_key,
|
||||
user: user
|
||||
|
||||
expect(valid_gpg_signature.reload).to have_attributes(
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: nil,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'gpg signature did not have an associated gpg key' do
|
||||
let!(:user) { create :user, email: GpgHelpers::User1.emails.first }
|
||||
|
||||
let!(:invalid_gpg_signature) do
|
||||
create :gpg_signature,
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: nil,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: false
|
||||
end
|
||||
|
||||
it 'updates the signature to being valid when the missing gpg key is added' do
|
||||
# InvalidGpgSignatureUpdater is called by the after_create hook
|
||||
gpg_key = create :gpg_key,
|
||||
key: GpgHelpers::User1.public_key,
|
||||
user: user
|
||||
|
||||
expect(invalid_gpg_signature.reload).to have_attributes(
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: gpg_key,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: true
|
||||
)
|
||||
end
|
||||
|
||||
it 'keeps the signature at being invalid when an unrelated gpg key is added' do
|
||||
# InvalidGpgSignatureUpdater is called by the after_create hook
|
||||
create :gpg_key,
|
||||
key: GpgHelpers::User2.public_key,
|
||||
user: user
|
||||
|
||||
expect(invalid_gpg_signature.reload).to have_attributes(
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: nil,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'gpg signature did have an associated unverified gpg key' do
|
||||
let!(:user) do
|
||||
create(:user, email: 'unrelated@example.com').tap do |user|
|
||||
user.skip_reconfirmation!
|
||||
end
|
||||
end
|
||||
|
||||
let!(:invalid_gpg_signature) do
|
||||
create :gpg_signature,
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: nil,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: false
|
||||
end
|
||||
|
||||
it 'updates the signature to being valid when the user updates the email address' do
|
||||
gpg_key = create :gpg_key,
|
||||
key: GpgHelpers::User1.public_key,
|
||||
user: user
|
||||
|
||||
expect(invalid_gpg_signature.reload.valid_signature).to be_falsey
|
||||
|
||||
# InvalidGpgSignatureUpdater is called by the after_update hook
|
||||
user.update_attributes!(email: GpgHelpers::User1.emails.first)
|
||||
|
||||
expect(invalid_gpg_signature.reload).to have_attributes(
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: gpg_key,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: true
|
||||
)
|
||||
end
|
||||
|
||||
it 'keeps the signature at being invalid when the changed email address is still unrelated' do
|
||||
gpg_key = create :gpg_key,
|
||||
key: GpgHelpers::User1.public_key,
|
||||
user: user
|
||||
|
||||
expect(invalid_gpg_signature.reload).to have_attributes(
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: gpg_key,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: false
|
||||
)
|
||||
|
||||
# InvalidGpgSignatureUpdater is called by the after_update hook
|
||||
user.update_attributes!(email: 'still.unrelated@example.com')
|
||||
|
||||
expect(invalid_gpg_signature.reload).to have_attributes(
|
||||
project: project,
|
||||
commit_sha: commit_sha,
|
||||
gpg_key: gpg_key,
|
||||
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
|
||||
valid_signature: false
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
83
spec/lib/gitlab/gpg_spec.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe Gitlab::Gpg do
|
||||
describe '.fingerprints_from_key' do
|
||||
before do
|
||||
# make sure that each method is using the temporary keychain
|
||||
expect(described_class).to receive(:using_tmp_keychain).and_call_original
|
||||
end
|
||||
|
||||
it 'returns CurrentKeyChain.fingerprints_from_key' do
|
||||
expect(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(GpgHelpers::User1.public_key)
|
||||
|
||||
described_class.fingerprints_from_key(GpgHelpers::User1.public_key)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.primary_keyids_from_key' do
|
||||
it 'returns the keyid' do
|
||||
expect(
|
||||
described_class.primary_keyids_from_key(GpgHelpers::User1.public_key)
|
||||
).to eq [GpgHelpers::User1.primary_keyid]
|
||||
end
|
||||
|
||||
it 'returns an empty array when the key is invalid' do
|
||||
expect(
|
||||
described_class.primary_keyids_from_key('bogus')
|
||||
).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
describe '.user_infos_from_key' do
|
||||
it 'returns the names and emails' do
|
||||
user_infos = described_class.user_infos_from_key(GpgHelpers::User1.public_key)
|
||||
expect(user_infos).to eq([{
|
||||
name: GpgHelpers::User1.names.first,
|
||||
email: GpgHelpers::User1.emails.first
|
||||
}])
|
||||
end
|
||||
|
||||
it 'returns an empty array when the key is invalid' do
|
||||
expect(
|
||||
described_class.user_infos_from_key('bogus')
|
||||
).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Gitlab::Gpg::CurrentKeyChain do
|
||||
around do |example|
|
||||
Gitlab::Gpg.using_tmp_keychain do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
describe '.add' do
|
||||
it 'stores the key in the keychain' do
|
||||
expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq []
|
||||
|
||||
described_class.add(GpgHelpers::User1.public_key)
|
||||
|
||||
keys = GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)
|
||||
expect(keys.count).to eq 1
|
||||
expect(keys.first).to have_attributes(
|
||||
email: GpgHelpers::User1.emails.first,
|
||||
fingerprint: GpgHelpers::User1.fingerprint
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.fingerprints_from_key' do
|
||||
it 'returns the fingerprint' do
|
||||
expect(
|
||||
described_class.fingerprints_from_key(GpgHelpers::User1.public_key)
|
||||
).to eq [GpgHelpers::User1.fingerprint]
|
||||
end
|
||||
|
||||
it 'returns an empty array when the key is invalid' do
|
||||
expect(
|
||||
described_class.fingerprints_from_key('bogus')
|
||||
).to eq []
|
||||
end
|
||||
end
|
||||
end
|
|
@ -91,6 +91,36 @@ describe Emails::Profile do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'user added gpg key' do
|
||||
let(:gpg_key) { create(:gpg_key) }
|
||||
|
||||
subject { Notify.new_gpg_key_email(gpg_key.id) }
|
||||
|
||||
it_behaves_like 'an email sent from GitLab'
|
||||
it_behaves_like 'it should not have Gmail Actions links'
|
||||
it_behaves_like 'a user cannot unsubscribe through footer link'
|
||||
|
||||
it 'is sent to the new user' do
|
||||
is_expected.to deliver_to gpg_key.user.email
|
||||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject /^GPG key was added to your account$/i
|
||||
end
|
||||
|
||||
it 'contains the new gpg key title' do
|
||||
is_expected.to have_body_text /#{gpg_key.fingerprint}/
|
||||
end
|
||||
|
||||
it 'includes a link to gpg keys page' do
|
||||
is_expected.to have_body_text /#{profile_gpg_keys_path}/
|
||||
end
|
||||
|
||||
context 'with GPG key that does not exist' do
|
||||
it { expect { Notify.new_gpg_key_email('foo') }.not_to raise_error }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'user added email' do
|
||||
let(:email) { create(:email) }
|
||||
|
||||
|
|
157
spec/models/gpg_key_spec.rb
Normal file
|
@ -0,0 +1,157 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe GpgKey do
|
||||
describe "associations" do
|
||||
it { is_expected.to belong_to(:user) }
|
||||
end
|
||||
|
||||
describe "validation" do
|
||||
it { is_expected.to validate_presence_of(:user) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:key) }
|
||||
it { is_expected.to validate_uniqueness_of(:key) }
|
||||
|
||||
it { is_expected.to allow_value("-----BEGIN PGP PUBLIC KEY BLOCK-----\nkey\n-----END PGP PUBLIC KEY BLOCK-----").for(:key) }
|
||||
|
||||
it { is_expected.not_to allow_value("-----BEGIN PGP PUBLIC KEY BLOCK-----\nkey").for(:key) }
|
||||
it { is_expected.not_to allow_value("-----BEGIN PGP PUBLIC KEY BLOCK-----\nkey\n-----BEGIN PGP PUBLIC KEY BLOCK-----").for(:key) }
|
||||
it { is_expected.not_to allow_value("-----BEGIN PGP PUBLIC KEY BLOCK----------END PGP PUBLIC KEY BLOCK-----").for(:key) }
|
||||
it { is_expected.not_to allow_value("-----BEGIN PGP PUBLIC KEY BLOCK-----").for(:key) }
|
||||
it { is_expected.not_to allow_value("-----END PGP PUBLIC KEY BLOCK-----").for(:key) }
|
||||
it { is_expected.not_to allow_value("key\n-----END PGP PUBLIC KEY BLOCK-----").for(:key) }
|
||||
it { is_expected.not_to allow_value('BEGIN PGP').for(:key) }
|
||||
end
|
||||
|
||||
context 'callbacks' do
|
||||
describe 'extract_fingerprint' do
|
||||
it 'extracts the fingerprint from the gpg key' do
|
||||
gpg_key = described_class.new(key: GpgHelpers::User1.public_key)
|
||||
gpg_key.valid?
|
||||
expect(gpg_key.fingerprint).to eq GpgHelpers::User1.fingerprint
|
||||
end
|
||||
end
|
||||
|
||||
describe 'extract_primary_keyid' do
|
||||
it 'extracts the primary keyid from the gpg key' do
|
||||
gpg_key = described_class.new(key: GpgHelpers::User1.public_key)
|
||||
gpg_key.valid?
|
||||
expect(gpg_key.primary_keyid).to eq GpgHelpers::User1.primary_keyid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#key=' do
|
||||
it 'strips white spaces' do
|
||||
key = <<~KEY.strip
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mQENBFMOSOgBCADFCYxmnXFbrDhfvlf03Q/bQuT+nZu46BFGbo7XkUjDowFXJQhP
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
KEY
|
||||
|
||||
expect(described_class.new(key: " #{key} ").key).to eq(key)
|
||||
end
|
||||
|
||||
it 'does not strip when the key is nil' do
|
||||
expect(described_class.new(key: nil).key).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_infos' do
|
||||
it 'returns the user infos from the gpg key' do
|
||||
gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key
|
||||
expect(Gitlab::Gpg).to receive(:user_infos_from_key).with(gpg_key.key)
|
||||
|
||||
gpg_key.user_infos
|
||||
end
|
||||
end
|
||||
|
||||
describe '#verified_user_infos' do
|
||||
it 'returns the user infos if it is verified' do
|
||||
user = create :user, email: GpgHelpers::User1.emails.first
|
||||
gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: user
|
||||
|
||||
expect(gpg_key.verified_user_infos).to eq([{
|
||||
name: GpgHelpers::User1.names.first,
|
||||
email: GpgHelpers::User1.emails.first
|
||||
}])
|
||||
end
|
||||
|
||||
it 'returns an empty array if the user info is not verified' do
|
||||
user = create :user, email: 'unrelated@example.com'
|
||||
gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: user
|
||||
|
||||
expect(gpg_key.verified_user_infos).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#emails_with_verified_status' do
|
||||
it 'email is verified if the user has the matching email' do
|
||||
user = create :user, email: 'bette.cartwright@example.com'
|
||||
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
|
||||
|
||||
expect(gpg_key.emails_with_verified_status).to eq(
|
||||
'bette.cartwright@example.com' => true,
|
||||
'bette.cartwright@example.net' => false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#verified?' do
|
||||
it 'returns true one of the email addresses in the key belongs to the user' do
|
||||
user = create :user, email: 'bette.cartwright@example.com'
|
||||
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
|
||||
|
||||
expect(gpg_key.verified?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if one of the email addresses in the key does not belong to the user' do
|
||||
user = create :user, email: 'someone.else@example.com'
|
||||
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
|
||||
|
||||
expect(gpg_key.verified?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe 'notification' do
|
||||
include EmailHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'sends a notification' do
|
||||
perform_enqueued_jobs do
|
||||
create(:gpg_key, user: user)
|
||||
end
|
||||
|
||||
should_email(user)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#revoke' do
|
||||
it 'invalidates all associated gpg signatures and destroys the key' do
|
||||
gpg_key = create :gpg_key
|
||||
gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: gpg_key
|
||||
|
||||
unrelated_gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key
|
||||
unrelated_gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: unrelated_gpg_key
|
||||
|
||||
gpg_key.revoke
|
||||
|
||||
expect(gpg_signature.reload).to have_attributes(
|
||||
valid_signature: false,
|
||||
gpg_key: nil
|
||||
)
|
||||
|
||||
expect(gpg_key.destroyed?).to be true
|
||||
|
||||
# unrelated signature is left untouched
|
||||
expect(unrelated_gpg_signature.reload).to have_attributes(
|
||||
valid_signature: true,
|
||||
gpg_key: unrelated_gpg_key
|
||||
)
|
||||
|
||||
expect(unrelated_gpg_key.destroyed?).to be false
|
||||
end
|
||||
end
|
||||
end
|
28
spec/models/gpg_signature_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe GpgSignature do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:project) }
|
||||
it { is_expected.to belong_to(:gpg_key) }
|
||||
end
|
||||
|
||||
describe 'validation' do
|
||||
subject { described_class.new }
|
||||
it { is_expected.to validate_presence_of(:commit_sha) }
|
||||
it { is_expected.to validate_presence_of(:project_id) }
|
||||
it { is_expected.to validate_presence_of(:gpg_key_primary_keyid) }
|
||||
end
|
||||
|
||||
describe '#commit' do
|
||||
it 'fetches the commit through the project' do
|
||||
commit_sha = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
|
||||
project = create :project
|
||||
commit = create :commit, project: project
|
||||
gpg_signature = create :gpg_signature, commit_sha: commit_sha
|
||||
|
||||
expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit)
|
||||
|
||||
gpg_signature.commit
|
||||
end
|
||||
end
|
||||
end
|
|
@ -350,6 +350,26 @@ describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'after update hook' do
|
||||
describe '.update_invalid_gpg_signatures' do
|
||||
let(:user) do
|
||||
create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
|
||||
user.skip_reconfirmation!
|
||||
end
|
||||
end
|
||||
|
||||
it 'does nothing when the name is updated' do
|
||||
expect(user).not_to receive(:update_invalid_gpg_signatures)
|
||||
user.update_attributes!(name: 'Bette')
|
||||
end
|
||||
|
||||
it 'synchronizes the gpg keys when the email is updated' do
|
||||
expect(user).to receive(:update_invalid_gpg_signatures)
|
||||
user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
|
||||
let(:request) { OpenStruct.new(remote_ip: "127.0.0.1") }
|
||||
let(:user) { create(:user) }
|
||||
|
|
|
@ -681,6 +681,24 @@ describe GitPushService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#update_signatures' do
|
||||
let(:service) do
|
||||
described_class.new(
|
||||
project,
|
||||
user,
|
||||
oldrev: sample_commit.parent_id,
|
||||
newrev: sample_commit.id,
|
||||
ref: 'refs/heads/master'
|
||||
)
|
||||
end
|
||||
|
||||
it 'calls CreateGpgSignatureWorker.perform_async for each commit' do
|
||||
expect(CreateGpgSignatureWorker).to receive(:perform_async).with(sample_commit.id, project.id)
|
||||
|
||||
execute_service(project, user, @oldrev, @newrev, @ref)
|
||||
end
|
||||
end
|
||||
|
||||
def execute_service(project, user, oldrev, newrev, ref)
|
||||
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
|
||||
service.execute
|
||||
|
|
|
@ -93,6 +93,18 @@ describe NotificationService do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GpgKeys' do
|
||||
describe '#new_gpg_key' do
|
||||
let!(:key) { create(:gpg_key) }
|
||||
|
||||
it { expect(notification.new_gpg_key(key)).to be_truthy }
|
||||
|
||||
it 'sends email to key owner' do
|
||||
expect{ notification.new_gpg_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Email' do
|
||||
describe '#new_email' do
|
||||
let!(:email) { create(:email) }
|
||||
|
|
202
spec/support/gpg_helpers.rb
Normal file
|
@ -0,0 +1,202 @@
|
|||
module GpgHelpers
|
||||
module User1
|
||||
extend self
|
||||
|
||||
def signed_commit_signature
|
||||
<<~SIGNATURE
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: GnuPG v1
|
||||
|
||||
iJwEAAECAAYFAliu264ACgkQzPvhnwCsix1VXgP9F6zwAMb3OXKZzqGxJ4MQIBoL
|
||||
OdiUSJpL/4sIA9uhFeIv3GIA+uhsG1BHHsG627+sDy7b8W9VWEd7tbcoz4Mvhf3P
|
||||
8g0AIt9/KJuStQZDrXwP1uP6Rrl759nDcNpoOKdSQ5EZ1zlRzeDROlZeDp7Ckfvw
|
||||
GLmN/74Gl3pk0wfgHFY=
|
||||
=wSgS
|
||||
-----END PGP SIGNATURE-----
|
||||
SIGNATURE
|
||||
end
|
||||
|
||||
def signed_commit_base_data
|
||||
<<~SIGNEDDATA
|
||||
tree ed60cfd202644fda1abaf684e7d965052db18c13
|
||||
parent caf6a0334a855e12f30205fff3d7333df1f65127
|
||||
author Nannie Bernhard <nannie.bernhard@example.com> 1487854510 +0100
|
||||
committer Nannie Bernhard <nannie.bernhard@example.com> 1487854510 +0100
|
||||
|
||||
signed commit, verified key/email
|
||||
SIGNEDDATA
|
||||
end
|
||||
|
||||
def secret_key
|
||||
<<~KEY.strip
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
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
|
||||
=b6UD
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
KEY
|
||||
end
|
||||
|
||||
def public_key
|
||||
<<~KEY.strip
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mI0EWK7VJwEEANSFayuVYenl7sBKUjmIxwDRc3jd+K+FWUZgknLgiLcevaLh/mxV
|
||||
98dLxDKGDHHNKc/B7Y4qdlZYv1wfNQVuIbd8dqUQFOOkH7ukbgcGBTxH+2IM67y+
|
||||
QBH618luS5Gz1d4bd0YoFf/xZGEh9G5xicz7TiXYzLKjnMjHu2EmbFePABEBAAG0
|
||||
LU5hbm5pZSBCZXJuaGFyZCA8bmFubmllLmJlcm5oYXJkQGV4YW1wbGUuY29tPoi4
|
||||
BBMBAgAiBQJYrtUnAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDM++Gf
|
||||
AKyLHaeSA/99oUWpb02PlfkALcx5RncboMHkgczYEU9wOFIgvXIReswThCMOvPZa
|
||||
piui+ItyJfV3ijJfO8IvbbFcvU7jjGA073Bb7tbzAEOQLA16mWgBLQlGaRWbHDW4
|
||||
uwFxvkJKA0GzEsadEXeniESaZPc4rOXKPO3+/MSQWS2bmvvGsBTEuriNBFiu1ScB
|
||||
BADIXkITf+kKCkD+n8tMsdTLInefu8KrJ8p7YRYCCabEXnWRsDb5zxUAG2VXCVUh
|
||||
Yl6QXQybkNiBaduS+uxilz7gtYZUMFJvQ09+fV7D2N9B7u/1bGdIYz+cDFJnEJit
|
||||
LY4w/nju2Sno5CL5Ead8sZuslKetSXPYHR/kbW462EOw5wARAQABiJ8EGAECAAkF
|
||||
Aliu1ScCGwwACgkQzPvhnwCsix2WRQQAtOXpBS60myrBUXhlcqabDQgSTw+Spbgb
|
||||
61hEMckyzpk7SfMNLz0EbYMvj9SU6znBG8RGeUljPTVMxPGr9yIpoFMSPKAUi/0K
|
||||
AgRmH3tVpxlMipwXjST1Jukk2eHckt/3jGw3E1ElMSFtULe6u5p4gu578hHukEwT
|
||||
IKzj0ZyC7DI=
|
||||
=Ug0r
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
KEY
|
||||
end
|
||||
|
||||
def primary_keyid
|
||||
fingerprint[-16..-1]
|
||||
end
|
||||
|
||||
def fingerprint
|
||||
'5F7EA3981A5845B141ABD522CCFBE19F00AC8B1D'
|
||||
end
|
||||
|
||||
def names
|
||||
['Nannie Bernhard']
|
||||
end
|
||||
|
||||
def emails
|
||||
['nannie.bernhard@example.com']
|
||||
end
|
||||
end
|
||||
|
||||
module User2
|
||||
extend self
|
||||
|
||||
def private_key
|
||||
<<~KEY.strip
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
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
|
||||
=yEqv
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
KEY
|
||||
end
|
||||
|
||||
def public_key
|
||||
<<~KEY.strip
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mI0EWK6qKgEEAODjqOSIBNYzL23WenEVYnvvGJs9d1SjfosBmBnq4a6eBLNwUvjn
|
||||
Rt0TIwwablEmnoMJiRmXBAOUz/9gSrB/QKar8CJsLmb1sevuF/mwzGXjSywnh+N3
|
||||
NPqh2KE7S/EPYBFKu2LCQRLMXGMnnWaM590+8AZ/S5zHOB5F9SUKszeTABEBAAG0
|
||||
L0JldHRlIENhcnR3cmlnaHQgPGJldHRlLmNhcnR3cmlnaHRAZXhhbXBsZS5jb20+
|
||||
iLgEEwECACIFAliuqioCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEL+d
|
||||
kl+RHv1lQhsD/0LDi008hCeoZdGMuwy2N5FMUusYg/X1tRQ6E0JcYYjvvE5oIZoX
|
||||
bVN0yLLD99P0rt6TSlt05CC7RPVPCNpKxKbF+TZMjMJiOi13XmY5Yti77ZMdBZtD
|
||||
7HeBzjkWCVuR6k8eIwy83niHGI6p6G87Q6XxvDjQ7wdBCNBpwkzmTYIBtC9CZXR0
|
||||
ZSBDYXJ0d3JpZ2h0IDxiZXR0ZS5jYXJ0d3JpZ2h0QGV4YW1wbGUubmV0Poi4BBMB
|
||||
AgAiBQJYrwLFAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRC/nZJfkR79
|
||||
ZVzsBADYe3Hv71I7+lTJDvKOTmV7M4ShOfjEpmDvL/5JB3FzXHAucCOlReCv/6+N
|
||||
afAspPTc1uys47dvtePwQalroAsd8Y1grn/PCh+Tx27kpn1WG8yZjUsuq3z5rrGR
|
||||
uXj6g8aeZdqkMTHUVF9Gd+g/5KDODdJbOjwH+l63b/bOiATDFbiNBFiuqioBBADe
|
||||
5nUdVOcbZlnxOjl0KBAT+A5bmyBLUT0BmLPsmA4PuXDSth7WvibPC8wcCdCYVk0I
|
||||
RMYneZUiWq/o5c4rthfLR4jg8kruvomQ4E4d4hyI6R0MLxXYZ3XMu67VuScFgbLU
|
||||
Rw1eRZ16ANd3Nc1VuFW7ms0vCG0idB8iSZBoULaK8QARAQABiJ8EGAECAAkFAliu
|
||||
qioCGwwACgkQv52SX5Ee/WVCCAQA1bBDCuVPYEUtZXwXH7CWDfX6ZyOROrQtpa7e
|
||||
i54CW6hKX3fYHpOiu4yHromjGlINTjjHclsJpn/qmiJizkB9/xl7IRWV+RVpDshZ
|
||||
3mymOldpLiKgI5+Rl6mZtO4TtM9BYziIEC24h+IkaLBylZYz4ogfbzXKF8JxtHPM
|
||||
UuwJVDE=
|
||||
=0vYo
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
KEY
|
||||
end
|
||||
|
||||
def primary_keyid
|
||||
fingerprint[-16..-1]
|
||||
end
|
||||
|
||||
def fingerprint
|
||||
'6D494CA6FC90C0CAE0910E42BF9D925F911EFD65'
|
||||
end
|
||||
|
||||
def names
|
||||
['Bette Cartwright', 'Bette Cartwright']
|
||||
end
|
||||
|
||||
def emails
|
||||
['bette.cartwright@example.com', 'bette.cartwright@example.net']
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ module TestEnv
|
|||
|
||||
# When developing the seed repository, comment out the branch you will modify.
|
||||
BRANCH_SHA = {
|
||||
'signed-commits' => '5d4a1cb',
|
||||
'not-merged-branch' => 'b83d6e3',
|
||||
'branch-merged' => '498214d',
|
||||
'empty-branch' => '7efb185',
|
||||
|
|
47
spec/workers/create_gpg_signature_worker_spec.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CreateGpgSignatureWorker do
|
||||
context 'when GpgKey is found' do
|
||||
it 'calls Commit#signature' do
|
||||
commit_sha = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
|
||||
project = create :project
|
||||
commit = instance_double(Commit)
|
||||
|
||||
allow(Project).to receive(:find_by).with(id: project.id).and_return(project)
|
||||
allow(project).to receive(:commit).with(commit_sha).and_return(commit)
|
||||
|
||||
expect(commit).to receive(:signature)
|
||||
|
||||
described_class.new.perform(commit_sha, project.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Commit is not found' do
|
||||
let(:nonexisting_commit_sha) { 'bogus' }
|
||||
let(:project) { create :project }
|
||||
|
||||
it 'does not raise errors' do
|
||||
expect { described_class.new.perform(nonexisting_commit_sha, project.id) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does not call Commit#signature' do
|
||||
expect_any_instance_of(Commit).not_to receive(:signature)
|
||||
|
||||
described_class.new.perform(nonexisting_commit_sha, project.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Project is not found' do
|
||||
let(:nonexisting_project_id) { -1 }
|
||||
|
||||
it 'does not raise errors' do
|
||||
expect { described_class.new.perform(anything, nonexisting_project_id) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does not call Commit#signature' do
|
||||
expect_any_instance_of(Commit).not_to receive(:signature)
|
||||
|
||||
described_class.new.perform(anything, nonexisting_project_id)
|
||||
end
|
||||
end
|
||||
end
|
29
spec/workers/invalid_gpg_signature_update_worker_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe InvalidGpgSignatureUpdateWorker do
|
||||
context 'when GpgKey is found' do
|
||||
it 'calls NotificationService.new.run' do
|
||||
gpg_key = create(:gpg_key)
|
||||
invalid_signature_updater = double(:invalid_signature_updater)
|
||||
|
||||
expect(Gitlab::Gpg::InvalidGpgSignatureUpdater).to receive(:new).with(gpg_key).and_return(invalid_signature_updater)
|
||||
expect(invalid_signature_updater).to receive(:run)
|
||||
|
||||
described_class.new.perform(gpg_key.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when GpgKey is not found' do
|
||||
let(:nonexisting_gpg_key_id) { -1 }
|
||||
|
||||
it 'does not raise errors' do
|
||||
expect { described_class.new.perform(nonexisting_gpg_key_id) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does not call NotificationService.new.run' do
|
||||
expect(Gitlab::Gpg::InvalidGpgSignatureUpdater).not_to receive(:new)
|
||||
|
||||
described_class.new.perform(nonexisting_gpg_key_id)
|
||||
end
|
||||
end
|
||||
end
|