From 817d9558febde484f23f623bd66d8c861ebc8236 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 14 Feb 2017 19:01:30 -0500 Subject: Prototype key verification --- Gemfile | 3 +++ Gemfile.lock | 3 +++ app/models/commit.rb | 12 ++++++++++++ app/views/projects/commit/_commit_box.html.haml | 1 + app/views/projects/commit/_signature.html.haml | 4 ++++ app/views/projects/commits/_commit.html.haml | 1 + lib/gitlab/git/commit.rb | 4 ++++ 7 files changed, 28 insertions(+) create mode 100644 app/views/projects/commit/_signature.html.haml diff --git a/Gemfile b/Gemfile index 43109de1b45..93934d03e42 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 6c2ac9368f2..77e87e2885f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 @@ -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) diff --git a/app/models/commit.rb b/app/models/commit.rb index 1e19f00106a..b22aaa3c461 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -234,6 +234,18 @@ class Commit @statuses[ref] = pipelines.latest_status(ref) end + def signature + return @signature if defined?(@signature) + + sig, signed = @raw.extract_signature(project.repository.raw_repository) + if sig && signed + GPGME::Crypto.new.verify(sig, signed_text: signed) do |sign| + @signature = sign + end + end + @signature ||= nil + end + def revert_branch_name "revert-#{short_id}" end diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 45109f2c58b..1a0c70ef803 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -6,6 +6,7 @@ = clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard")) %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} + = render partial: 'signature', object: @commit.signature %span= s_('ByAuthor|by') = author_avatar(@commit, size: 24) %strong diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml new file mode 100644 index 00000000000..7335b6b9597 --- /dev/null +++ b/app/views/projects/commit/_signature.html.haml @@ -0,0 +1,4 @@ +- if signature + %a.btn.disabled.btn-xs{ class: ('btn-success' if signature.valid?) } + %i.fa.fa-key{ class: ('fa-inverse' if signature.valid?) } + = signature.valid? ? 'Verified': 'Unverified' diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1033bad0d49..5f67727514a 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -39,6 +39,7 @@ .commit-actions.flex-row.hidden-xs - if commit.status(ref) = render_commit_status(commit, ref: ref) + = render partial: 'projects/commit/signature', object: commit.signature = 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) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 09511cc6504..d19f55c423b 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -319,6 +319,10 @@ module Gitlab end end + def extract_signature(repo) + Rugged::Commit.extract_signature(repo.rugged, sha) + end + def stats Gitlab::Git::CommitStats.new(self) end -- cgit v1.2.1 From 28bb5e3d53a585b1fb958d1d91622da0a038bea8 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 16 Feb 2017 15:39:37 +0100 Subject: commit signature with spec --- app/models/commit.rb | 2 +- lib/gitlab/git/commit.rb | 7 +- spec/models/commit_spec.rb | 40 ++++++++ spec/spec_helper.rb | 12 +++ spec/support/gpg_helpers.rb | 222 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 spec/support/gpg_helpers.rb diff --git a/app/models/commit.rb b/app/models/commit.rb index b22aaa3c461..0d50a32d138 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -237,7 +237,7 @@ class Commit def signature return @signature if defined?(@signature) - sig, signed = @raw.extract_signature(project.repository.raw_repository) + sig, signed = @raw.signature(project.repository) if sig && signed GPGME::Crypto.new.verify(sig, signed_text: signed) do |sign| @signature = sign diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index d19f55c423b..4dcaff5e0a0 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -319,7 +319,12 @@ module Gitlab end end - def extract_signature(repo) + # Get the gpg signature of this commit. + # + # Ex. + # commit.signature(repo) + # + def signature(repo) Rugged::Commit.extract_signature(repo.rugged, sha) end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 528b211c9d6..0fc00ab4f18 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -414,4 +414,44 @@ eos expect(described_class.valid_hash?('a' * 41)).to be false end end + + describe '#signature' do + it 'returns nil if the commit is not signed' do + expect(commit.signature).to be_nil + end + + context 'signed commit', :gpg do + it 'returns a valid signature if the public key is known' do + GPGME::Key.import(GpgHelpers.public_key) + + raw_commit = double(:raw_commit, signature: [ + GpgHelpers.signed_commit_signature, + GpgHelpers.signed_commit_base_data + ]) + allow(raw_commit).to receive :save! + + commit = create :commit, + git_commit: raw_commit, + project: project + + expect(commit.signature).to be_a GPGME::Signature + expect(commit.signature.valid?).to be_truthy + end + + it 'returns an invalid signature if the public commit is unknown', :gpg do + raw_commit = double(:raw_commit, signature: [ + GpgHelpers.signed_commit_signature, + GpgHelpers.signed_commit_base_data + ]) + allow(raw_commit).to receive :save! + + commit = create :commit, + git_commit: raw_commit, + project: project + + expect(commit.signature).to be_a GPGME::Signature + expect(commit.signature.valid?).to be_falsey + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e7329210896..6b4ec608efb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -141,6 +141,18 @@ RSpec.configure do |config| config.around(:each, :postgresql) do |example| example.run if Gitlab::Database.postgresql? end + + config.around(:each, :gpg) do |example| + Dir.mktmpdir do |dir| + original_dir = GPGME::Engine.dirinfo('homedir') + + GPGME::Engine.home_dir = dir + + example.run + + GPGME::Engine.home_dir = original_dir + end + end end FactoryGirl::SyntaxRunner.class_eval do diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb new file mode 100644 index 00000000000..3b50d831b00 --- /dev/null +++ b/spec/support/gpg_helpers.rb @@ -0,0 +1,222 @@ +module GpgHelpers + extend self + + def signed_commit_signature + <<~SIGNATURE + -----BEGIN PGP SIGNATURE----- + Version: GnuPG v1 + + iQEcBAABAgAGBQJYpIi9AAoJEMcorxCXLpfAZZIH/R/nhcC4s0j6nqAsi9Kbc4DX + TGZyfjed6puWzqnT90Vy+WyUC7FjWJpkuOKQz+NQD9JcBMRp/OC0GtkNz4djv1se + Nup29qWd+Fg2XGEBakTxAo2e9cg38a2rGEIL6V8i+tYAhDt5OyLdzD/XsF0vt02E + ZikSvV02c6ByrjPq37ZdOgnk1xJrS1NM0Sn4B7L3cAz6TYb1OvyG1Z4HnMWgTBHy + e/uKLPRYhx7a4D4TEt4/JWN3sb0VnaToG623EdJ1APF/MK9Es+H7YfgBsyu18nss + 705F+PZ2vx/1b9z5dLc/jQNf+k9vQH4uhmOFwUJnuQ/qB4/3H/UyLH/HfomK7Zk= + =fzCF + -----END PGP SIGNATURE----- + SIGNATURE + end + + def signed_commit_base_data + <<~SIGNEDDATA + tree ed60cfd202644fda1abaf684e7d965052db18c13 + parent 4ded8b5ce09d2b665e5893945b29d8d626691086 + author Alexis Reigel 1487177917 +0100 + committer Alexis Reigel 1487177917 +0100 + + signed commit, verified key/email + SIGNEDDATA + end + + def public_key + <<~PUBLICKEY + -----BEGIN PGP PUBLIC KEY BLOCK----- + Version: GnuPG v1 + + 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 + =1CGB + -----END PGP PUBLIC KEY BLOCK----- + PUBLICKEY + end +end -- cgit v1.2.1 From fbf1fd1a204a24aef2b80473ec64a520ed2a2dfc Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 22 Feb 2017 12:49:17 +0100 Subject: add gpg key model --- app/models/gpg_key.rb | 35 ++++++++++++++++++++++++ db/migrate/20170222111732_create_gpg_keys.rb | 13 +++++++++ db/schema.rb | 11 ++++++++ spec/models/gpg_key_spec.rb | 41 ++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 app/models/gpg_key.rb create mode 100644 db/migrate/20170222111732_create_gpg_keys.rb create mode 100644 spec/models/gpg_key_spec.rb diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb new file mode 100644 index 00000000000..16de0b542d7 --- /dev/null +++ b/app/models/gpg_key.rb @@ -0,0 +1,35 @@ +class GpgKey < ActiveRecord::Base + KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze + + belongs_to :user + + validates :fingerprint, + presence: true, + uniqueness: true + + validates :key, + presence: true, + uniqueness: true, + format: { + with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX}).)+\Z/m + } + + before_validation :extract_fingerprint + + def key=(value) + value.strip! unless value.blank? + write_attribute(:key, value) + end + + private + + def extract_fingerprint + import = GPGME::Key.import(key) + + return if import.considered == 0 + + # we can assume that the result only contains one item as the validation + # only allows one key + self.fingerprint = import.imports.first.fingerprint + end +end diff --git a/db/migrate/20170222111732_create_gpg_keys.rb b/db/migrate/20170222111732_create_gpg_keys.rb new file mode 100644 index 00000000000..1b8b7a91fe1 --- /dev/null +++ b/db/migrate/20170222111732_create_gpg_keys.rb @@ -0,0 +1,13 @@ +class CreateGpgKeys < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :gpg_keys do |t| + t.string :fingerprint + t.text :key + t.references :user, index: true, foreign_key: true + + t.timestamps_with_timezone null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1ec25c7d46f..54f98559243 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -540,6 +540,16 @@ 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.string "fingerprint" + t.text "key" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree + create_table "identities", force: :cascade do |t| t.string "extern_uid" t.string "provider" @@ -1602,6 +1612,7 @@ 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" 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 diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb new file mode 100644 index 00000000000..02623c52fa6 --- /dev/null +++ b/spec/models/gpg_key_spec.rb @@ -0,0 +1,41 @@ +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(:fingerprint) } + + 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").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').for(:key) } + end + + context 'callbacks' do + describe 'extract_fingerprint' do + it 'extracts the fingerprint from the gpg key', :gpg do + gpg_key = described_class.new(key: GpgHelpers.public_key) + gpg_key.valid? + expect(gpg_key.fingerprint).to eq '4F4840A503964251CF7D7F5DC728AF10972E97C0' + 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 + end +end -- cgit v1.2.1 From 7b7cd6f69d59092a55fc8b293edf09638fba20d9 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 22 Feb 2017 15:37:49 +0100 Subject: add emails method to GgpKey --- app/models/gpg_key.rb | 5 +++++ spec/factories/gpg_keys.rb | 7 +++++++ spec/models/gpg_key_spec.rb | 8 ++++++++ 3 files changed, 20 insertions(+) create mode 100644 spec/factories/gpg_keys.rb diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 16de0b542d7..d77051850f3 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -21,6 +21,11 @@ class GpgKey < ActiveRecord::Base write_attribute(:key, value) end + def emails + raw_key = GPGME::Key.get(fingerprint) + raw_key.uids.map(&:email) + end + private def extract_fingerprint diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb new file mode 100644 index 00000000000..e43a3c19672 --- /dev/null +++ b/spec/factories/gpg_keys.rb @@ -0,0 +1,7 @@ +require_relative '../support/gpg_helpers' + +FactoryGirl.define do + factory :gpg_key do + key GpgHelpers.public_key + end +end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 02623c52fa6..24ef291a021 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -38,4 +38,12 @@ describe GpgKey do expect(described_class.new(key: " #{key} ").key).to eq(key) end end + + describe '#emails' do + it 'returns the emails from the gpg key' do + gpg_key = create :gpg_key + + expect(gpg_key.emails).to match_array %w(mail@koffeinfrei.org lex@panter.ch) + end + end end -- cgit v1.2.1 From ab4120de3165ea262de726aa3e102b74951d2bca Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 22 Feb 2017 16:22:39 +0100 Subject: only validate gpg_key#fingerprint "internally" --- app/models/gpg_key.rb | 14 +++++++++----- spec/models/gpg_key_spec.rb | 2 -- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index d77051850f3..b012db1428f 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -3,17 +3,21 @@ class GpgKey < ActiveRecord::Base belongs_to :user - validates :fingerprint, - presence: true, - uniqueness: true - validates :key, presence: true, uniqueness: true, format: { - with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX}).)+\Z/m + with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX}).)+\Z/m, + message: "is invalid. A valid public GPG key begins with '#{KEY_PREFIX}'" } + 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) } + before_validation :extract_fingerprint def key=(value) diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 24ef291a021..1c5dd95ba65 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -6,8 +6,6 @@ describe GpgKey do end describe "validation" do - it { is_expected.to validate_presence_of(:fingerprint) } - 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").for(:key) } -- cgit v1.2.1 From 7b4d29f4b5b02b5aee3e3cbfc8282965a38c4622 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 22 Feb 2017 16:24:48 +0100 Subject: add profile gpg key page to manage gpg keys --- app/controllers/profiles/gpg_keys_controller.rb | 33 +++++++++++++++++++ app/models/user.rb | 1 + app/views/layouts/nav/_profile.html.haml | 4 +++ app/views/profiles/gpg_keys/_form.html.haml | 10 ++++++ app/views/profiles/gpg_keys/_key.html.haml | 13 ++++++++ app/views/profiles/gpg_keys/_key_table.html.haml | 11 +++++++ app/views/profiles/gpg_keys/index.html.haml | 18 +++++++++++ config/routes/profile.rb | 1 + spec/features/profiles/gpg_keys_spec.rb | 40 ++++++++++++++++++++++++ 9 files changed, 131 insertions(+) create mode 100644 app/controllers/profiles/gpg_keys_controller.rb create mode 100644 app/views/profiles/gpg_keys/_form.html.haml create mode 100644 app/views/profiles/gpg_keys/_key.html.haml create mode 100644 app/views/profiles/gpg_keys/_key_table.html.haml create mode 100644 app/views/profiles/gpg_keys/index.html.haml create mode 100644 spec/features/profiles/gpg_keys_spec.rb diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb new file mode 100644 index 00000000000..b04c14a6993 --- /dev/null +++ b/app/controllers/profiles/gpg_keys_controller.rb @@ -0,0 +1,33 @@ +class Profiles::GpgKeysController < Profiles::ApplicationController + 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 + @gpp_key = current_user.gpg_keys.find(params[:id]) + @gpp_key.destroy + + 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 +end diff --git a/app/models/user.rb b/app/models/user.rb index c26be6d05a2..5aebd36cf8a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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, dependent: :destroy has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 424905ea890..26d9640e98a 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -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 diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml new file mode 100644 index 00000000000..3fcf563d970 --- /dev/null +++ b/app/views/profiles/gpg_keys/_form.html.haml @@ -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" diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml new file mode 100644 index 00000000000..fc167698ccd --- /dev/null +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -0,0 +1,13 @@ +%li.key-list-item + .pull-left.append-right-10 + = icon 'key', class: "settings-list-icon hidden-xs" + .key-list-item-info + = key.emails.join(' ') + .description + = 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?' }, method: :delete, class: "btn btn-transparent prepend-left-10" do + %span.sr-only Remove + = icon('trash') diff --git a/app/views/profiles/gpg_keys/_key_table.html.haml b/app/views/profiles/gpg_keys/_key_table.html.haml new file mode 100644 index 00000000000..cabb92c5a24 --- /dev/null +++ b/app/views/profiles/gpg_keys/_key_table.html.haml @@ -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. diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml new file mode 100644 index 00000000000..30066522766 --- /dev/null +++ b/app/views/profiles/gpg_keys/index.html.haml @@ -0,0 +1,18 @@ +- 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 an GPG key + = render 'form' + %hr + %h5 + Your GPG keys (#{@gpg_keys.count}) + .append-bottom-default + = render 'key_table' diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 3dc890e5785..00388b9c0cd 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -23,6 +23,7 @@ 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] resources :emails, only: [:index, :create, :destroy] resources :chat_names, only: [:index, :new, :create, :destroy] do collection do diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb new file mode 100644 index 00000000000..223f2e81842 --- /dev/null +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +feature 'Profile > GPG Keys', :gpg do + let(:user) { create(:user) } + + 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: attributes_for(:gpg_key)[:key]) + click_button('Add key') + + expect(page).to have_content('mail@koffeinfrei.org lex@panter.ch') + expect(page).to have_content('4F4840A503964251CF7D7F5DC728AF10972E97C0') + end + end + + scenario 'User sees their keys' do + create(:gpg_key, user: user) + visit profile_gpg_keys_path + + expect(page).to have_content('mail@koffeinfrei.org lex@panter.ch') + expect(page).to have_content('4F4840A503964251CF7D7F5DC728AF10972E97C0') + end + + scenario 'User removes a key via the key index' do + create(:gpg_key, user: user) + visit profile_gpg_keys_path + + click_link('Remove') + + expect(page).to have_content('Your GPG keys (0)') + end +end -- cgit v1.2.1 From e34cef0cd2fcf9a01d3f3b6dd215bbcc25d65d27 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 22 Feb 2017 17:20:42 +0100 Subject: extract gpg functionality to lib class --- app/models/gpg_key.rb | 6 +----- lib/gitlab/gpg.rb | 32 ++++++++++++++++++++++++++++++++ spec/lib/gitlab/gpg_spec.rb | 20 ++++++++++++++++++++ spec/spec_helper.rb | 8 +------- 4 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 lib/gitlab/gpg.rb create mode 100644 spec/lib/gitlab/gpg_spec.rb diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index b012db1428f..aa0e8883a47 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -33,12 +33,8 @@ class GpgKey < ActiveRecord::Base private def extract_fingerprint - import = GPGME::Key.import(key) - - return if import.considered == 0 - # we can assume that the result only contains one item as the validation # only allows one key - self.fingerprint = import.imports.first.fingerprint + self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first end end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb new file mode 100644 index 00000000000..373ef79ab85 --- /dev/null +++ b/lib/gitlab/gpg.rb @@ -0,0 +1,32 @@ +module Gitlab + module Gpg + extend self + + def fingerprints_from_key(key) + using_tmp_keychain do + import = GPGME::Key.import(key) + + return [] if import.imported == 0 + + import.imports.map(&:fingerprint) + 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 diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb new file mode 100644 index 00000000000..a59302e6738 --- /dev/null +++ b/spec/lib/gitlab/gpg_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +describe Gitlab::Gpg do + describe '.fingerprints_from_key' do + it 'returns the fingerprint' do + expect( + described_class.fingerprints_from_key(GpgHelpers.public_key) + ).to eq ['4F4840A503964251CF7D7F5DC728AF10972E97C0'] + end + + it 'returns an empty array when the key is invalid' do + expect( + described_class.fingerprints_from_key('bogus') + ).to eq [] + end + end + + describe '.add_to_keychain' do + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6b4ec608efb..a0df233507b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -143,14 +143,8 @@ RSpec.configure do |config| end config.around(:each, :gpg) do |example| - Dir.mktmpdir do |dir| - original_dir = GPGME::Engine.dirinfo('homedir') - - GPGME::Engine.home_dir = dir - + Gitlab::Gpg.using_tmp_keychain do example.run - - GPGME::Engine.home_dir = original_dir end end end -- cgit v1.2.1 From 87c0fd34557463528a552986a42f4ebb52d3bd56 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 22 Feb 2017 18:36:25 +0100 Subject: add / remove gpg keys to / from system keychain --- app/models/gpg_key.rb | 10 ++++++++++ lib/gitlab/gpg.rb | 8 ++++++++ spec/lib/gitlab/gpg_spec.rb | 20 +++++++++++++++++++- spec/models/gpg_key_spec.rb | 24 +++++++++++++++++++++--- spec/support/gpg_helpers.rb | 2 +- 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index aa0e8883a47..a9f1400650c 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -19,6 +19,8 @@ class GpgKey < ActiveRecord::Base unless: -> { errors.has_key?(:key) } before_validation :extract_fingerprint + after_create :add_to_keychain + after_destroy :remove_from_keychain def key=(value) value.strip! unless value.blank? @@ -37,4 +39,12 @@ class GpgKey < ActiveRecord::Base # only allows one key self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first end + + def add_to_keychain + Gitlab::Gpg.add_to_keychain(key) + end + + def remove_from_keychain + Gitlab::Gpg.remove_from_keychain(fingerprint) + end end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 373ef79ab85..64f18d00e46 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -12,6 +12,14 @@ module Gitlab end end + def add_to_keychain(key) + GPGME::Key.import(key) + end + + def remove_from_keychain(fingerprint) + GPGME::Key.get(fingerprint).delete! + end + def using_tmp_keychain Dir.mktmpdir do |dir| @original_dirs ||= [GPGME::Engine.dirinfo('homedir')] diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index a59302e6738..2f779492c24 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -15,6 +15,24 @@ describe Gitlab::Gpg do end end - describe '.add_to_keychain' do + describe '.add_to_keychain', :gpg do + it 'stores the key in the keychain' do + expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).to eq [] + + Gitlab::Gpg.add_to_keychain(GpgHelpers.public_key) + + expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).not_to eq [] + end + end + + describe '.remove_from_keychain', :gpg do + it 'removes the key from the keychain' do + Gitlab::Gpg.add_to_keychain(GpgHelpers.public_key) + expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).not_to eq [] + + Gitlab::Gpg.remove_from_keychain('4F4840A503964251CF7D7F5DC728AF10972E97C0') + + expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).to eq [] + end end end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 1c5dd95ba65..facdf91550f 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -13,14 +13,32 @@ describe GpgKey do it { is_expected.not_to allow_value('BEGIN PGP').for(:key) } end - context 'callbacks' do + context 'callbacks', :gpg do describe 'extract_fingerprint' do - it 'extracts the fingerprint from the gpg key', :gpg do + it 'extracts the fingerprint from the gpg key' do gpg_key = described_class.new(key: GpgHelpers.public_key) gpg_key.valid? expect(gpg_key.fingerprint).to eq '4F4840A503964251CF7D7F5DC728AF10972E97C0' end end + + describe 'add_to_keychain' do + it 'calls add_to_keychain after create' do + expect(Gitlab::Gpg).to receive(:add_to_keychain).with(GpgHelpers.public_key) + create :gpg_key + end + end + + describe 'remove_from_keychain' do + it 'calls remove_from_keychain after destroy' do + allow(Gitlab::Gpg).to receive :add_to_keychain + gpg_key = create :gpg_key + + expect(Gitlab::Gpg).to receive(:remove_from_keychain).with('4F4840A503964251CF7D7F5DC728AF10972E97C0') + + gpg_key.destroy! + end + end end describe '#key=' do @@ -37,7 +55,7 @@ describe GpgKey do end end - describe '#emails' do + describe '#emails', :gpg do it 'returns the emails from the gpg key' do gpg_key = create :gpg_key diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb index 3b50d831b00..2f440488546 100644 --- a/spec/support/gpg_helpers.rb +++ b/spec/support/gpg_helpers.rb @@ -29,7 +29,7 @@ module GpgHelpers end def public_key - <<~PUBLICKEY + <<~PUBLICKEY.strip -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1 -- cgit v1.2.1 From eb77e1068c09cf8ef45689720a2bf200542b8024 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 23 Feb 2017 12:02:36 +0100 Subject: add second gpg key for specs --- spec/factories/gpg_keys.rb | 2 +- spec/lib/gitlab/gpg_spec.rb | 6 +- spec/models/commit_spec.rb | 10 +- spec/models/gpg_key_spec.rb | 4 +- spec/support/gpg_helpers.rb | 510 ++++++++++++++++++++++++++------------------ 5 files changed, 309 insertions(+), 223 deletions(-) diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb index e43a3c19672..70c2875b985 100644 --- a/spec/factories/gpg_keys.rb +++ b/spec/factories/gpg_keys.rb @@ -2,6 +2,6 @@ require_relative '../support/gpg_helpers' FactoryGirl.define do factory :gpg_key do - key GpgHelpers.public_key + key GpgHelpers::User1.public_key end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 2f779492c24..04a434a993d 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Gpg do describe '.fingerprints_from_key' do it 'returns the fingerprint' do expect( - described_class.fingerprints_from_key(GpgHelpers.public_key) + described_class.fingerprints_from_key(GpgHelpers::User1.public_key) ).to eq ['4F4840A503964251CF7D7F5DC728AF10972E97C0'] end @@ -19,7 +19,7 @@ describe Gitlab::Gpg do it 'stores the key in the keychain' do expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).to eq [] - Gitlab::Gpg.add_to_keychain(GpgHelpers.public_key) + Gitlab::Gpg.add_to_keychain(GpgHelpers::User1.public_key) expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).not_to eq [] end @@ -27,7 +27,7 @@ describe Gitlab::Gpg do describe '.remove_from_keychain', :gpg do it 'removes the key from the keychain' do - Gitlab::Gpg.add_to_keychain(GpgHelpers.public_key) + Gitlab::Gpg.add_to_keychain(GpgHelpers::User1.public_key) expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).not_to eq [] Gitlab::Gpg.remove_from_keychain('4F4840A503964251CF7D7F5DC728AF10972E97C0') diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0fc00ab4f18..3c6ce49b48d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -422,11 +422,11 @@ eos context 'signed commit', :gpg do it 'returns a valid signature if the public key is known' do - GPGME::Key.import(GpgHelpers.public_key) + GPGME::Key.import(GpgHelpers::User1.public_key) raw_commit = double(:raw_commit, signature: [ - GpgHelpers.signed_commit_signature, - GpgHelpers.signed_commit_base_data + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data ]) allow(raw_commit).to receive :save! @@ -440,8 +440,8 @@ eos it 'returns an invalid signature if the public commit is unknown', :gpg do raw_commit = double(:raw_commit, signature: [ - GpgHelpers.signed_commit_signature, - GpgHelpers.signed_commit_base_data + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data ]) allow(raw_commit).to receive :save! diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index facdf91550f..6bfd0b0d4f6 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -16,7 +16,7 @@ describe GpgKey do context 'callbacks', :gpg do describe 'extract_fingerprint' do it 'extracts the fingerprint from the gpg key' do - gpg_key = described_class.new(key: GpgHelpers.public_key) + gpg_key = described_class.new(key: GpgHelpers::User1.public_key) gpg_key.valid? expect(gpg_key.fingerprint).to eq '4F4840A503964251CF7D7F5DC728AF10972E97C0' end @@ -24,7 +24,7 @@ describe GpgKey do describe 'add_to_keychain' do it 'calls add_to_keychain after create' do - expect(Gitlab::Gpg).to receive(:add_to_keychain).with(GpgHelpers.public_key) + expect(Gitlab::Gpg).to receive(:add_to_keychain).with(GpgHelpers::User1.public_key) create :gpg_key end end diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb index 2f440488546..375f4415846 100644 --- a/spec/support/gpg_helpers.rb +++ b/spec/support/gpg_helpers.rb @@ -1,222 +1,308 @@ module GpgHelpers - extend self + module User1 + extend self - def signed_commit_signature - <<~SIGNATURE - -----BEGIN PGP SIGNATURE----- - Version: GnuPG v1 + def signed_commit_signature + <<~SIGNATURE + -----BEGIN PGP SIGNATURE----- + Version: GnuPG v1 - iQEcBAABAgAGBQJYpIi9AAoJEMcorxCXLpfAZZIH/R/nhcC4s0j6nqAsi9Kbc4DX - TGZyfjed6puWzqnT90Vy+WyUC7FjWJpkuOKQz+NQD9JcBMRp/OC0GtkNz4djv1se - Nup29qWd+Fg2XGEBakTxAo2e9cg38a2rGEIL6V8i+tYAhDt5OyLdzD/XsF0vt02E - ZikSvV02c6ByrjPq37ZdOgnk1xJrS1NM0Sn4B7L3cAz6TYb1OvyG1Z4HnMWgTBHy - e/uKLPRYhx7a4D4TEt4/JWN3sb0VnaToG623EdJ1APF/MK9Es+H7YfgBsyu18nss - 705F+PZ2vx/1b9z5dLc/jQNf+k9vQH4uhmOFwUJnuQ/qB4/3H/UyLH/HfomK7Zk= - =fzCF - -----END PGP SIGNATURE----- - SIGNATURE - end + iQEcBAABAgAGBQJYpIi9AAoJEMcorxCXLpfAZZIH/R/nhcC4s0j6nqAsi9Kbc4DX + TGZyfjed6puWzqnT90Vy+WyUC7FjWJpkuOKQz+NQD9JcBMRp/OC0GtkNz4djv1se + Nup29qWd+Fg2XGEBakTxAo2e9cg38a2rGEIL6V8i+tYAhDt5OyLdzD/XsF0vt02E + ZikSvV02c6ByrjPq37ZdOgnk1xJrS1NM0Sn4B7L3cAz6TYb1OvyG1Z4HnMWgTBHy + e/uKLPRYhx7a4D4TEt4/JWN3sb0VnaToG623EdJ1APF/MK9Es+H7YfgBsyu18nss + 705F+PZ2vx/1b9z5dLc/jQNf+k9vQH4uhmOFwUJnuQ/qB4/3H/UyLH/HfomK7Zk= + =fzCF + -----END PGP SIGNATURE----- + SIGNATURE + end + + def signed_commit_base_data + <<~SIGNEDDATA + tree ed60cfd202644fda1abaf684e7d965052db18c13 + parent 4ded8b5ce09d2b665e5893945b29d8d626691086 + author Alexis Reigel 1487177917 +0100 + committer Alexis Reigel 1487177917 +0100 + + signed commit, verified key/email + SIGNEDDATA + end - def signed_commit_base_data - <<~SIGNEDDATA - tree ed60cfd202644fda1abaf684e7d965052db18c13 - parent 4ded8b5ce09d2b665e5893945b29d8d626691086 - author Alexis Reigel 1487177917 +0100 - committer Alexis Reigel 1487177917 +0100 + def public_key + <<~PUBLICKEY.strip + -----BEGIN PGP PUBLIC KEY BLOCK----- + Version: GnuPG v1 - signed commit, verified key/email - SIGNEDDATA + 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 + =1CGB + -----END PGP PUBLIC KEY BLOCK----- + PUBLICKEY + end + + def key_id + '972E97C0' + end end - def public_key - <<~PUBLICKEY.strip - -----BEGIN PGP PUBLIC KEY BLOCK----- - Version: GnuPG v1 + 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 + 7HeBzjkWCVuR6k8eIwy83niHGI6p6G87Q6XxvDjQ7wdBCNBpwkzmTYIBuI0EWK6q + KgEEAN7mdR1U5xtmWfE6OXQoEBP4DlubIEtRPQGYs+yYDg+5cNK2Hta+Js8LzBwJ + 0JhWTQhExid5lSJar+jlziu2F8tHiODySu6+iZDgTh3iHIjpHQwvFdhndcy7rtW5 + JwWBstRHDV5FnXoA13c1zVW4VbuazS8IbSJ0HyJJkGhQtorxABEBAAGInwQYAQIA + CQUCWK6qKgIbDAAKCRC/nZJfkR79ZUIIBADVsEMK5U9gRS1lfBcfsJYN9fpnI5E6 + tC2lrt6LngJbqEpfd9gek6K7jIeuiaMaUg1OOMdyWwmmf+qaImLOQH3/GXshFZX5 + FWkOyFnebKY6V2kuIqAjn5GXqZm07hO0z0FjOIgQLbiH4iRosHKVljPiiB9vNcoX + wnG0c8xS7AlUMQ== + =Erp5 + -----END PGP PUBLIC KEY BLOCK----- + KEY + end + + def key_id + '911EFD65' + end - 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 - =1CGB - -----END PGP PUBLIC KEY BLOCK----- - PUBLICKEY + def signature + '6D494CA6FC90C0CAE0910E42BF9D925F911EFD65' + end end end -- cgit v1.2.1 From 597ae6e2208a4910544e5877db7fff300a058886 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 23 Feb 2017 12:03:18 +0100 Subject: feature spec for gpg signed commits --- spec/features/commits_spec.rb | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index fb1e47994ef..c303f29a832 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'fileutils' describe 'Commits' do include CiStatusHelper @@ -203,4 +204,35 @@ describe 'Commits' do end end end + + describe 'GPG signed commits' do + let(:user) { create(:user) } + + before do + project.team << [user, :master] + login_with(user) + end + + it 'shows the signed status', :gpg do + GPGME::Key.import(GpgHelpers::User1.public_key) + + # FIXME: add this to the test repository directly + remote_path = project.repository.path_to_repo + Dir.mktmpdir do |dir| + FileUtils.cd dir do + `git clone --quiet #{remote_path} .` + `git commit --quiet -S#{GpgHelpers::User1.key_id} --allow-empty -m "signed commit, verified key/email"` + `git commit --quiet -S#{GpgHelpers::User2.key_id} --allow-empty -m "signed commit, unverified key/email"` + `git push --quiet` + end + end + + visit namespace_project_commits_path(project.namespace, project, :master) + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).to have_content 'Verified' + end + end + end end -- cgit v1.2.1 From 5ce61120b19f7f12e7aff714857851f57571ce0e Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 23 Feb 2017 14:07:51 +0100 Subject: use example gpg key instead of my own --- spec/features/profiles/gpg_keys_spec.rb | 8 +- spec/lib/gitlab/gpg_spec.rb | 12 +- spec/models/gpg_key_spec.rb | 8 +- spec/support/gpg_helpers.rb | 275 +++++++++----------------------- 4 files changed, 92 insertions(+), 211 deletions(-) diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index 223f2e81842..42e1a6624b7 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -16,8 +16,8 @@ feature 'Profile > GPG Keys', :gpg do fill_in('Key', with: attributes_for(:gpg_key)[:key]) click_button('Add key') - expect(page).to have_content('mail@koffeinfrei.org lex@panter.ch') - expect(page).to have_content('4F4840A503964251CF7D7F5DC728AF10972E97C0') + expect(page).to have_content(GpgHelpers::User1.email) + expect(page).to have_content(GpgHelpers::User1.fingerprint) end end @@ -25,8 +25,8 @@ feature 'Profile > GPG Keys', :gpg do create(:gpg_key, user: user) visit profile_gpg_keys_path - expect(page).to have_content('mail@koffeinfrei.org lex@panter.ch') - expect(page).to have_content('4F4840A503964251CF7D7F5DC728AF10972E97C0') + expect(page).to have_content(GpgHelpers::User1.email) + expect(page).to have_content(GpgHelpers::User1.fingerprint) end scenario 'User removes a key via the key index' do diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 04a434a993d..6cfc634e2d9 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Gpg do it 'returns the fingerprint' do expect( described_class.fingerprints_from_key(GpgHelpers::User1.public_key) - ).to eq ['4F4840A503964251CF7D7F5DC728AF10972E97C0'] + ).to eq [GpgHelpers::User1.fingerprint] end it 'returns an empty array when the key is invalid' do @@ -17,22 +17,22 @@ describe Gitlab::Gpg do describe '.add_to_keychain', :gpg do it 'stores the key in the keychain' do - expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).to eq [] + expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq [] Gitlab::Gpg.add_to_keychain(GpgHelpers::User1.public_key) - expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).not_to eq [] + expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).not_to eq [] end end describe '.remove_from_keychain', :gpg do it 'removes the key from the keychain' do Gitlab::Gpg.add_to_keychain(GpgHelpers::User1.public_key) - expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).not_to eq [] + expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).not_to eq [] - Gitlab::Gpg.remove_from_keychain('4F4840A503964251CF7D7F5DC728AF10972E97C0') + Gitlab::Gpg.remove_from_keychain(GpgHelpers::User1.fingerprint) - expect(GPGME::Key.find(:public, '4F4840A503964251CF7D7F5DC728AF10972E97C0')).to eq [] + expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq [] end end end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 6bfd0b0d4f6..917d420878a 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -18,7 +18,7 @@ describe GpgKey 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 '4F4840A503964251CF7D7F5DC728AF10972E97C0' + expect(gpg_key.fingerprint).to eq GpgHelpers::User1.fingerprint end end @@ -34,7 +34,7 @@ describe GpgKey do allow(Gitlab::Gpg).to receive :add_to_keychain gpg_key = create :gpg_key - expect(Gitlab::Gpg).to receive(:remove_from_keychain).with('4F4840A503964251CF7D7F5DC728AF10972E97C0') + expect(Gitlab::Gpg).to receive(:remove_from_keychain).with(GpgHelpers::User1.fingerprint) gpg_key.destroy! end @@ -57,9 +57,9 @@ describe GpgKey do describe '#emails', :gpg do it 'returns the emails from the gpg key' do - gpg_key = create :gpg_key + gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key - expect(gpg_key.emails).to match_array %w(mail@koffeinfrei.org lex@panter.ch) + expect(gpg_key.emails).to eq [GpgHelpers::User1.email] end end end diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb index 375f4415846..8e005634c94 100644 --- a/spec/support/gpg_helpers.rb +++ b/spec/support/gpg_helpers.rb @@ -7,13 +7,11 @@ module GpgHelpers -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 - iQEcBAABAgAGBQJYpIi9AAoJEMcorxCXLpfAZZIH/R/nhcC4s0j6nqAsi9Kbc4DX - TGZyfjed6puWzqnT90Vy+WyUC7FjWJpkuOKQz+NQD9JcBMRp/OC0GtkNz4djv1se - Nup29qWd+Fg2XGEBakTxAo2e9cg38a2rGEIL6V8i+tYAhDt5OyLdzD/XsF0vt02E - ZikSvV02c6ByrjPq37ZdOgnk1xJrS1NM0Sn4B7L3cAz6TYb1OvyG1Z4HnMWgTBHy - e/uKLPRYhx7a4D4TEt4/JWN3sb0VnaToG623EdJ1APF/MK9Es+H7YfgBsyu18nss - 705F+PZ2vx/1b9z5dLc/jQNf+k9vQH4uhmOFwUJnuQ/qB4/3H/UyLH/HfomK7Zk= - =fzCF + iJwEAAECAAYFAliu264ACgkQzPvhnwCsix1VXgP9F6zwAMb3OXKZzqGxJ4MQIBoL + OdiUSJpL/4sIA9uhFeIv3GIA+uhsG1BHHsG627+sDy7b8W9VWEd7tbcoz4Mvhf3P + 8g0AIt9/KJuStQZDrXwP1uP6Rrl759nDcNpoOKdSQ5EZ1zlRzeDROlZeDp7Ckfvw + GLmN/74Gl3pk0wfgHFY= + =wSgS -----END PGP SIGNATURE----- SIGNATURE end @@ -21,208 +19,87 @@ module GpgHelpers def signed_commit_base_data <<~SIGNEDDATA tree ed60cfd202644fda1abaf684e7d965052db18c13 - parent 4ded8b5ce09d2b665e5893945b29d8d626691086 - author Alexis Reigel 1487177917 +0100 - committer Alexis Reigel 1487177917 +0100 + parent caf6a0334a855e12f30205fff3d7333df1f65127 + author Nannie Bernhard 1487854510 +0100 + committer Nannie Bernhard 1487854510 +0100 signed commit, verified key/email SIGNEDDATA end + def secret_key + <<~KEY.strip + -----BEGIN PGP PRIVATE KEY BLOCK----- + Version: GnuPG v1 + + lQHYBFiu1ScBBADUhWsrlWHp5e7ASlI5iMcA0XN43fivhVlGYJJy4Ii3Hr2i4f5s + VffHS8QyhgxxzSnPwe2OKnZWWL9cHzUFbiG3fHalEBTjpB+7pG4HBgU8R/tiDOu8 + vkAR+tfJbkuRs9XeG3dGKBX/8WRhIfRucYnM+04l2Myyo5zIx7thJmxXjwARAQAB + AAP/XUtcqrtfSnDYCK4Xvo4e3msUSAEZxOPDNzP51lhfbBQgp7qSGDj9Fw5ZyNwz + 5llse3nksT5OyMUY7HX+rq2UOs12a/piLqvhtX1okp/oTAETmKXNYkZLenv6t94P + NqLi0o2AnXAvL9ueXa7WUY3l4DkvuLcjT4+9Ut2Y71zIjeECAN7q9ohNL7E8tNkf + Elsbx+8KfyHRQXiSUYaQLlvDRq2lYCKIS7sogTqjZMEgbZx2mRX1fakRlvcmqOwB + QoX34zcCAPQPd+yTteNUV12uvDaj8V9DICktPPhbHdYYaUoHjF8RrIHCTRUPzk9E + KzCL9dUP8eXPPBV/ty+zjUwl69IgCmkB/3pnNZ0D4EJsNgu24UgI0N+c8H/PE1D6 + K+bGQ/jK83uYPMXJUsiojssCHLGNp7eBGHFn1PpEqZphgVI50ZMrZQWhJbQtTmFu + bmllIEJlcm5oYXJkIDxuYW5uaWUuYmVybmhhcmRAZXhhbXBsZS5jb20+iLgEEwEC + ACIFAliu1ScCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEMz74Z8ArIsd + p5ID/32hRalvTY+V+QAtzHlGdxugweSBzNgRT3A4UiC9chF6zBOEIw689lqmK6L4 + i3Il9XeKMl87wi9tsVy9TuOMYDTvcFvu1vMAQ5AsDXqZaAEtCUZpFZscNbi7AXG+ + QkoDQbMSxp0Rd6eIRJpk9zis5co87f78xJBZLZua+8awFMS6nQHYBFiu1ScBBADI + XkITf+kKCkD+n8tMsdTLInefu8KrJ8p7YRYCCabEXnWRsDb5zxUAG2VXCVUhYl6Q + XQybkNiBaduS+uxilz7gtYZUMFJvQ09+fV7D2N9B7u/1bGdIYz+cDFJnEJitLY4w + /nju2Sno5CL5Ead8sZuslKetSXPYHR/kbW462EOw5wARAQABAAP+IoZfU1XUdVbr + +RPWp3ny5SekviDPu8co9BZ4ANTh5+8wyfA3oNbGUxTlYthoU07MZYqq+/k63R28 + 6HgVGC3gdvCiRMGmryIQ6roLLRXkfzjXrI7Lgnhx4OtVjo62pAKDqdl45wEa1Q+M + v08CQF6XNpb5R9Xszz4aBC4eV0KjtjkCANlGSQHZ1B81g+iltj1FAhRHkyUFlrc1 + cqLVhNgxtHZ96+R57Uk2A7dIJBsE00eIYaHOfk5X5GD/95s1QvPcQskCAOwUk5xj + NeQ6VV/1+cI91TrWU6VnT2Yj8632fM/JlKKfaS15pp8t5Ha6pNFr3xD4KgQutchq + fPsEOjaU7nwQ/i8B/1rDPTYfNXFpRNt33WAB1XtpgOIHlpmOfaYYqf6lneTlZWBc + TgyO+j+ZsHAvP18ugIRkU8D192NflzgAGwXLryijyYifBBgBAgAJBQJYrtUnAhsM + AAoJEMz74Z8ArIsdlkUEALTl6QUutJsqwVF4ZXKmmw0IEk8PkqW4G+tYRDHJMs6Z + O0nzDS89BG2DL4/UlOs5wRvERnlJYz01TMTxq/ciKaBTEjygFIv9CgIEZh97VacZ + TIqcF40k9SbpJNnh3JLf94xsNxNRJTEhbVC3uruaeILue/IR7pBMEyCs49Gcguwy + =b6UD + -----END PGP PRIVATE KEY BLOCK----- + KEY + end + def public_key - <<~PUBLICKEY.strip + <<~KEY.strip -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1 - 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 - =1CGB + 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----- - PUBLICKEY + KEY end def key_id - '972E97C0' + '00AC8B1D' + end + + def fingerprint + '5F7EA3981A5845B141ABD522CCFBE19F00AC8B1D' + end + + def email + 'nannie.bernhard@example.com' end end @@ -301,8 +178,12 @@ module GpgHelpers '911EFD65' end - def signature + def fingerprint '6D494CA6FC90C0CAE0910E42BF9D925F911EFD65' end + + def email + 'bette.cartwright@example.com' + end end end -- cgit v1.2.1 From 41c96c45f2696af54dde81271741a6342db5d55a Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 24 Feb 2017 20:17:08 +0100 Subject: test with a gpg key with multiple emails --- spec/features/profiles/gpg_keys_spec.rb | 4 ++-- spec/models/gpg_key_spec.rb | 2 +- spec/support/gpg_helpers.rb | 31 ++++++++++++++++++------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index 42e1a6624b7..e7939533272 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -16,7 +16,7 @@ feature 'Profile > GPG Keys', :gpg do fill_in('Key', with: attributes_for(:gpg_key)[:key]) click_button('Add key') - expect(page).to have_content(GpgHelpers::User1.email) + expect(page).to have_content(GpgHelpers::User1.emails.join) expect(page).to have_content(GpgHelpers::User1.fingerprint) end end @@ -25,7 +25,7 @@ feature 'Profile > GPG Keys', :gpg do create(:gpg_key, user: user) visit profile_gpg_keys_path - expect(page).to have_content(GpgHelpers::User1.email) + expect(page).to have_content(GpgHelpers::User1.emails.join) expect(page).to have_content(GpgHelpers::User1.fingerprint) end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 917d420878a..889396c19c3 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -59,7 +59,7 @@ describe GpgKey do it 'returns the emails from the gpg key' do gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key - expect(gpg_key.emails).to eq [GpgHelpers::User1.email] + expect(gpg_key.emails).to eq GpgHelpers::User1.emails end end end diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb index 8e005634c94..52c478e1976 100644 --- a/spec/support/gpg_helpers.rb +++ b/spec/support/gpg_helpers.rb @@ -98,8 +98,8 @@ module GpgHelpers '5F7EA3981A5845B141ABD522CCFBE19F00AC8B1D' end - def email - 'nannie.bernhard@example.com' + def emails + ['nannie.bernhard@example.com'] end end @@ -161,15 +161,20 @@ module GpgHelpers iLgEEwECACIFAliuqioCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEL+d kl+RHv1lQhsD/0LDi008hCeoZdGMuwy2N5FMUusYg/X1tRQ6E0JcYYjvvE5oIZoX bVN0yLLD99P0rt6TSlt05CC7RPVPCNpKxKbF+TZMjMJiOi13XmY5Yti77ZMdBZtD - 7HeBzjkWCVuR6k8eIwy83niHGI6p6G87Q6XxvDjQ7wdBCNBpwkzmTYIBuI0EWK6q - KgEEAN7mdR1U5xtmWfE6OXQoEBP4DlubIEtRPQGYs+yYDg+5cNK2Hta+Js8LzBwJ - 0JhWTQhExid5lSJar+jlziu2F8tHiODySu6+iZDgTh3iHIjpHQwvFdhndcy7rtW5 - JwWBstRHDV5FnXoA13c1zVW4VbuazS8IbSJ0HyJJkGhQtorxABEBAAGInwQYAQIA - CQUCWK6qKgIbDAAKCRC/nZJfkR79ZUIIBADVsEMK5U9gRS1lfBcfsJYN9fpnI5E6 - tC2lrt6LngJbqEpfd9gek6K7jIeuiaMaUg1OOMdyWwmmf+qaImLOQH3/GXshFZX5 - FWkOyFnebKY6V2kuIqAjn5GXqZm07hO0z0FjOIgQLbiH4iRosHKVljPiiB9vNcoX - wnG0c8xS7AlUMQ== - =Erp5 + 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 @@ -182,8 +187,8 @@ module GpgHelpers '6D494CA6FC90C0CAE0910E42BF9D925F911EFD65' end - def email - 'bette.cartwright@example.com' + def emails + ['bette.cartwright@example.com', 'bette.cartwright@example.net'] end end end -- cgit v1.2.1 From 0e3d3d60bae48f3698f9e7b0e060edb67170b11e Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 24 Feb 2017 20:07:57 +0100 Subject: email handling for gpg keys --- app/models/gpg_key.rb | 3 +-- lib/gitlab/gpg.rb | 20 ++++++++++++++++++++ spec/lib/gitlab/gpg_spec.rb | 26 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index a9f1400650c..146ca2f2705 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -28,8 +28,7 @@ class GpgKey < ActiveRecord::Base end def emails - raw_key = GPGME::Key.get(fingerprint) - raw_key.uids.map(&:email) + Gitlab::Gpg::CurrentKeyChain.emails(fingerprint) end private diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 64f18d00e46..73a4b691cff 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -2,6 +2,14 @@ module Gitlab module Gpg extend self + module CurrentKeyChain + extend self + + def emails(fingerprint) + GPGME::Key.find(:public, fingerprint).flat_map { |raw_key| raw_key.uids.map(&:email) } + end + end + def fingerprints_from_key(key) using_tmp_keychain do import = GPGME::Key.import(key) @@ -12,6 +20,18 @@ module Gitlab end end + def emails_from_key(key) + using_tmp_keychain do + import = GPGME::Key.import(key) + + return [] if import.imported == 0 + + fingerprints = import.imports.map(&:fingerprint) + + GPGME::Key.find(:public, fingerprints).flat_map { |raw_key| raw_key.uids.map(&:email) } + end + end + def add_to_keychain(key) GPGME::Key.import(key) end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 6cfc634e2d9..2a55e7d89de 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -15,6 +15,20 @@ describe Gitlab::Gpg do end end + describe '.emails_from_key' do + it 'returns the emails' do + expect( + described_class.emails_from_key(GpgHelpers::User1.public_key) + ).to eq GpgHelpers::User1.emails + end + + it 'returns an empty array when the key is invalid' do + expect( + described_class.emails_from_key('bogus') + ).to eq [] + end + end + describe '.add_to_keychain', :gpg do it 'stores the key in the keychain' do expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq [] @@ -36,3 +50,15 @@ describe Gitlab::Gpg do end end end + +describe Gitlab::Gpg::CurrentKeyChain, :gpg do + describe '.emails' do + it 'returns the emails' do + Gitlab::Gpg.add_to_keychain(GpgHelpers::User2.public_key) + + expect( + described_class.emails(GpgHelpers::User2.fingerprint) + ).to match_array GpgHelpers::User2.emails + end + end +end -- cgit v1.2.1 From 0668521b2b8ed32f4a3f192a8ad04c64f6c1c0cd Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 24 Feb 2017 20:16:04 +0100 Subject: move current keychain methods to namespace --- app/models/gpg_key.rb | 4 ++-- lib/gitlab/gpg.rb | 16 ++++++++-------- spec/lib/gitlab/gpg_spec.rb | 34 +++++++++++++++++----------------- spec/models/gpg_key_spec.rb | 8 +++++--- 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 146ca2f2705..1101dbae4a9 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -40,10 +40,10 @@ class GpgKey < ActiveRecord::Base end def add_to_keychain - Gitlab::Gpg.add_to_keychain(key) + Gitlab::Gpg::CurrentKeyChain.add(key) end def remove_from_keychain - Gitlab::Gpg.remove_from_keychain(fingerprint) + Gitlab::Gpg::CurrentKeyChain.remove(fingerprint) end end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 73a4b691cff..f478f1ae5d8 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -5,6 +5,14 @@ module Gitlab module CurrentKeyChain extend self + def add(key) + GPGME::Key.import(key) + end + + def remove(fingerprint) + GPGME::Key.get(fingerprint).delete! + end + def emails(fingerprint) GPGME::Key.find(:public, fingerprint).flat_map { |raw_key| raw_key.uids.map(&:email) } end @@ -32,14 +40,6 @@ module Gitlab end end - def add_to_keychain(key) - GPGME::Key.import(key) - end - - def remove_from_keychain(fingerprint) - GPGME::Key.get(fingerprint).delete! - end - def using_tmp_keychain Dir.mktmpdir do |dir| @original_dirs ||= [GPGME::Engine.dirinfo('homedir')] diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 2a55e7d89de..c0df719c0c2 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -28,37 +28,37 @@ describe Gitlab::Gpg do ).to eq [] end end +end + +describe Gitlab::Gpg::CurrentKeyChain, :gpg do + describe '.emails' do + it 'returns the emails' do + Gitlab::Gpg::CurrentKeyChain.add(GpgHelpers::User2.public_key) + + expect( + described_class.emails(GpgHelpers::User2.fingerprint) + ).to match_array GpgHelpers::User2.emails + end + end - describe '.add_to_keychain', :gpg do + describe '.add', :gpg do it 'stores the key in the keychain' do expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq [] - Gitlab::Gpg.add_to_keychain(GpgHelpers::User1.public_key) + Gitlab::Gpg::CurrentKeyChain.add(GpgHelpers::User1.public_key) expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).not_to eq [] end end - describe '.remove_from_keychain', :gpg do + describe '.remove', :gpg do it 'removes the key from the keychain' do - Gitlab::Gpg.add_to_keychain(GpgHelpers::User1.public_key) + Gitlab::Gpg::CurrentKeyChain.add(GpgHelpers::User1.public_key) expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).not_to eq [] - Gitlab::Gpg.remove_from_keychain(GpgHelpers::User1.fingerprint) + Gitlab::Gpg::CurrentKeyChain.remove(GpgHelpers::User1.fingerprint) expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq [] end end end - -describe Gitlab::Gpg::CurrentKeyChain, :gpg do - describe '.emails' do - it 'returns the emails' do - Gitlab::Gpg.add_to_keychain(GpgHelpers::User2.public_key) - - expect( - described_class.emails(GpgHelpers::User2.fingerprint) - ).to match_array GpgHelpers::User2.emails - end - end -end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 889396c19c3..e8c41299937 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -24,17 +24,19 @@ describe GpgKey do describe 'add_to_keychain' do it 'calls add_to_keychain after create' do - expect(Gitlab::Gpg).to receive(:add_to_keychain).with(GpgHelpers::User1.public_key) + expect(Gitlab::Gpg::CurrentKeyChain).to receive(:add).with(GpgHelpers::User1.public_key) create :gpg_key end end describe 'remove_from_keychain' do it 'calls remove_from_keychain after destroy' do - allow(Gitlab::Gpg).to receive :add_to_keychain + allow(Gitlab::Gpg::CurrentKeyChain).to receive :add gpg_key = create :gpg_key - expect(Gitlab::Gpg).to receive(:remove_from_keychain).with(GpgHelpers::User1.fingerprint) + expect( + Gitlab::Gpg::CurrentKeyChain + ).to receive(:remove).with(GpgHelpers::User1.fingerprint) gpg_key.destroy! end -- cgit v1.2.1 From f0fe1b9d4397e6c1c6aa2da6e371e234db774fe2 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 24 Feb 2017 21:28:26 +0100 Subject: gpg email verification --- app/helpers/badges_helper.rb | 11 ++++++++ app/models/gpg_key.rb | 16 +++++++++++ app/views/profiles/gpg_keys/_key.html.haml | 2 +- spec/features/profiles/gpg_keys_spec.rb | 20 +++++++------ spec/models/gpg_key_spec.rb | 45 ++++++++++++++++++++++++++++-- 5 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 app/helpers/badges_helper.rb diff --git a/app/helpers/badges_helper.rb b/app/helpers/badges_helper.rb new file mode 100644 index 00000000000..e1c8927ab54 --- /dev/null +++ b/app/helpers/badges_helper.rb @@ -0,0 +1,11 @@ +module BadgesHelper + def verified_email_badge(email, verified) + css_classes = %w(btn btn-xs disabled) + + css_classes << 'btn-success' if verified + + content_tag 'span', class: css_classes do + "#{email} #{verified ? 'Verified' : 'Unverified'}" + end + end +end diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 1101dbae4a9..04c7ce2e79f 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -31,6 +31,18 @@ class GpgKey < ActiveRecord::Base Gitlab::Gpg::CurrentKeyChain.emails(fingerprint) end + def emails_with_verified_status + emails_in_key_chain = emails + emails_from_key = Gitlab::Gpg.emails_from_key(key) + + emails_from_key.map do |email| + [ + email, + email == user.email && emails_in_key_chain.include?(email) + ] + end + end + private def extract_fingerprint @@ -40,6 +52,10 @@ class GpgKey < ActiveRecord::Base end def add_to_keychain + emails_from_key = Gitlab::Gpg.emails_from_key(key) + + return unless emails_from_key.include?(user.email) + Gitlab::Gpg::CurrentKeyChain.add(key) end diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index fc167698ccd..d7450a22f4c 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -2,7 +2,7 @@ .pull-left.append-right-10 = icon 'key', class: "settings-list-icon hidden-xs" .key-list-item-info - = key.emails.join(' ') + = key.emails_with_verified_status.map { |email, verified| verified_email_badge(email, verified) }.join(' ').html_safe .description = key.fingerprint .pull-right diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index e7939533272..552cca4a84e 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' feature 'Profile > GPG Keys', :gpg do - let(:user) { create(:user) } + let(:user) { create(:user, email: GpgHelpers::User2.emails.first) } before do login_as(user) @@ -13,24 +13,26 @@ feature 'Profile > GPG Keys', :gpg do end scenario 'saves the new key' do - fill_in('Key', with: attributes_for(:gpg_key)[:key]) + fill_in('Key', with: GpgHelpers::User2.public_key) click_button('Add key') - expect(page).to have_content(GpgHelpers::User1.emails.join) - expect(page).to have_content(GpgHelpers::User1.fingerprint) + 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 keys' do - create(:gpg_key, user: user) + 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(GpgHelpers::User1.emails.join) - expect(page).to have_content(GpgHelpers::User1.fingerprint) + 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) + create(:gpg_key, user: user, key: GpgHelpers::User2.public_key) visit profile_gpg_keys_path click_link('Remove') diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index e8c41299937..695a2f65c09 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -23,9 +23,20 @@ describe GpgKey do end describe 'add_to_keychain' do - it 'calls add_to_keychain after create' do - expect(Gitlab::Gpg::CurrentKeyChain).to receive(:add).with(GpgHelpers::User1.public_key) - create :gpg_key + context "user's email matches one of the key's emails" do + it 'calls .add after create' do + expect(Gitlab::Gpg::CurrentKeyChain).to receive(:add).with(GpgHelpers::User2.public_key) + user = create :user, email: GpgHelpers::User2.emails.first + create :gpg_key, user: user, key: GpgHelpers::User2.public_key + end + end + + context "user's email does not match one of the key's emails" do + it 'does not call .add after create' do + expect(Gitlab::Gpg::CurrentKeyChain).not_to receive(:add) + user = create :user + create :gpg_key, user: user, key: GpgHelpers::User2.public_key + end end end @@ -64,4 +75,32 @@ describe GpgKey do expect(gpg_key.emails).to eq GpgHelpers::User1.emails end end + + describe '#emails_with_verified_status', :gpg do + context 'key is in the keychain' 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 match_array [ + ['bette.cartwright@example.com', true], + ['bette.cartwright@example.net', false] + ] + end + end + + context 'key is in not the keychain' do + it 'emails are unverified' do + user = create :user, email: 'bette.cartwright@example.com' + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user + + Gitlab::Gpg::CurrentKeyChain.remove(GpgHelpers::User2.fingerprint) + + expect(gpg_key.emails_with_verified_status).to match_array [ + ['bette.cartwright@example.com', false], + ['bette.cartwright@example.net', false] + ] + end + end + end end -- cgit v1.2.1 From c1281982bd7975b45bed5b8e2c5ef5e242ea18fd Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 28 Feb 2017 10:49:59 +0100 Subject: notification email on add new gpg key --- app/mailers/emails/profile.rb | 10 ++++++++++ app/models/gpg_key.rb | 7 +++++++ app/services/notification_service.rb | 10 ++++++++++ app/views/notify/new_gpg_key_email.html.haml | 10 ++++++++++ app/views/notify/new_gpg_key_email.text.erb | 7 +++++++ spec/factories/gpg_keys.rb | 1 + spec/mailers/emails/profile_spec.rb | 30 ++++++++++++++++++++++++++++ spec/models/gpg_key_spec.rb | 14 +++++++++++++ spec/services/notification_service_spec.rb | 12 +++++++++++ 9 files changed, 101 insertions(+) create mode 100644 app/views/notify/new_gpg_key_email.html.haml create mode 100644 app/views/notify/new_gpg_key_email.text.erb diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 256cbcd73a1..4580e1c83bd 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -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 diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 04c7ce2e79f..83a303ae953 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -1,4 +1,6 @@ class GpgKey < ActiveRecord::Base + include AfterCommitQueue + KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze belongs_to :user @@ -20,6 +22,7 @@ class GpgKey < ActiveRecord::Base before_validation :extract_fingerprint after_create :add_to_keychain + after_create :notify_user after_destroy :remove_from_keychain def key=(value) @@ -62,4 +65,8 @@ class GpgKey < ActiveRecord::Base def remove_from_keychain Gitlab::Gpg::CurrentKeyChain.remove(fingerprint) end + + def notify_user + run_after_commit { NotificationService.new.new_gpg_key(self) } + end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 3a98a5f6b64..b94921d2a08 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -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 diff --git a/app/views/notify/new_gpg_key_email.html.haml b/app/views/notify/new_gpg_key_email.html.haml new file mode 100644 index 00000000000..4b9350c4e88 --- /dev/null +++ b/app/views/notify/new_gpg_key_email.html.haml @@ -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 diff --git a/app/views/notify/new_gpg_key_email.text.erb b/app/views/notify/new_gpg_key_email.text.erb new file mode 100644 index 00000000000..80b5a1fd7ff --- /dev/null +++ b/app/views/notify/new_gpg_key_email.text.erb @@ -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 %> diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb index 70c2875b985..1258dce8940 100644 --- a/spec/factories/gpg_keys.rb +++ b/spec/factories/gpg_keys.rb @@ -3,5 +3,6 @@ require_relative '../support/gpg_helpers' FactoryGirl.define do factory :gpg_key do key GpgHelpers::User1.public_key + user end end diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index 8c1c9bf135f..09e5094cf84 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -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) } diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 695a2f65c09..4292892da4f 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -103,4 +103,18 @@ describe GpgKey do end 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 end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 4fc5eb0a527..0f07a89aad9 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -93,6 +93,18 @@ describe NotificationService, services: true 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) } -- cgit v1.2.1 From 8bd94a7304d392ad030295b5dfcd84c0100eddd1 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 28 Feb 2017 15:25:12 +0100 Subject: remove gpg from keychain when user's email changes --- app/models/gpg_key.rb | 31 ++++++++++------- app/models/user.rb | 7 ++++ lib/gitlab/gpg.rb | 4 ++- spec/features/commits_spec.rb | 16 +++++++-- spec/models/gpg_key_spec.rb | 80 ++++++++++++++++++++++++++++++------------- spec/models/user_spec.rb | 20 +++++++++++ 6 files changed, 118 insertions(+), 40 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 83a303ae953..8332ba3ee6e 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -21,9 +21,9 @@ class GpgKey < ActiveRecord::Base unless: -> { errors.has_key?(:key) } before_validation :extract_fingerprint - after_create :add_to_keychain + after_create :synchronize_keychain after_create :notify_user - after_destroy :remove_from_keychain + after_destroy :synchronize_keychain def key=(value) value.strip! unless value.blank? @@ -31,21 +31,32 @@ class GpgKey < ActiveRecord::Base end def emails - Gitlab::Gpg::CurrentKeyChain.emails(fingerprint) + @emails ||= Gitlab::Gpg.emails_from_key(key) end - def emails_with_verified_status - emails_in_key_chain = emails - emails_from_key = Gitlab::Gpg.emails_from_key(key) + def emails_in_keychain + @emails_in_keychain ||= Gitlab::Gpg::CurrentKeyChain.emails(fingerprint) + end - emails_from_key.map do |email| + def emails_with_verified_status + emails.map do |email| [ email, - email == user.email && emails_in_key_chain.include?(email) + email == user.email && emails_in_keychain.include?(email) ] end end + def synchronize_keychain + if emails.include?(user.email) + add_to_keychain + else + remove_from_keychain + end + + @emails_in_keychain = nil + end + private def extract_fingerprint @@ -55,10 +66,6 @@ class GpgKey < ActiveRecord::Base end def add_to_keychain - emails_from_key = Gitlab::Gpg.emails_from_key(key) - - return unless emails_from_key.include?(user.email) - Gitlab::Gpg::CurrentKeyChain.add(key) end diff --git a/app/models/user.rb b/app/models/user.rb index 5aebd36cf8a..42c83e3d206 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -155,6 +155,7 @@ class User < ActiveRecord::Base before_validation :set_public_email, if: :public_email_changed? after_update :update_emails_with_primary_email, if: :email_changed? + after_update :synchronize_gpg_keys, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: :external_changed? after_save :ensure_namespace_correct @@ -1157,4 +1158,10 @@ class User < ActiveRecord::Base ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) end + + def synchronize_gpg_keys + gpg_keys.each do |gpg_key| + gpg_key.synchronize_keychain + end + end end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index f478f1ae5d8..ee0467ae264 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -10,7 +10,9 @@ module Gitlab end def remove(fingerprint) - GPGME::Key.get(fingerprint).delete! + # `#get` raises an EOFError if the keychain is empty, which is why we + # use the friendlier `#find` + GPGME::Key.find(:public, fingerprint).each(&:delete!) end def emails(fingerprint) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index c303f29a832..79952eda2ff 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -206,7 +206,8 @@ describe 'Commits' do end describe 'GPG signed commits' do - let(:user) { create(:user) } + let!(:user) { create :user, email: GpgHelpers::User1.emails.first } + let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key, user: user } before do project.team << [user, :master] @@ -214,8 +215,6 @@ describe 'Commits' do end it 'shows the signed status', :gpg do - GPGME::Key.import(GpgHelpers::User1.public_key) - # FIXME: add this to the test repository directly remote_path = project.repository.path_to_repo Dir.mktmpdir do |dir| @@ -233,6 +232,17 @@ describe 'Commits' do expect(page).to have_content 'Unverified' expect(page).to have_content 'Verified' end + + # user changes his email which makes the gpg key unverified + user.skip_reconfirmation! + user.update_attributes!(email: 'bette.cartwright@example.org') + + visit namespace_project_commits_path(project.namespace, project, :master) + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).not_to have_content 'Verified' + end end end end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 4292892da4f..18746ad9d88 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -22,33 +22,16 @@ describe GpgKey do end end - describe 'add_to_keychain' do - context "user's email matches one of the key's emails" do - it 'calls .add after create' do - expect(Gitlab::Gpg::CurrentKeyChain).to receive(:add).with(GpgHelpers::User2.public_key) - user = create :user, email: GpgHelpers::User2.emails.first - create :gpg_key, user: user, key: GpgHelpers::User2.public_key - end + describe 'synchronize_keychain' do + it 'calls #synchronize_keychain after create' do + gpg_key = build :gpg_key + expect(gpg_key).to receive(:synchronize_keychain) + gpg_key.save! end - context "user's email does not match one of the key's emails" do - it 'does not call .add after create' do - expect(Gitlab::Gpg::CurrentKeyChain).not_to receive(:add) - user = create :user - create :gpg_key, user: user, key: GpgHelpers::User2.public_key - end - end - end - - describe 'remove_from_keychain' do - it 'calls remove_from_keychain after destroy' do - allow(Gitlab::Gpg::CurrentKeyChain).to receive :add + it 'calls #remove_from_keychain after destroy' do gpg_key = create :gpg_key - - expect( - Gitlab::Gpg::CurrentKeyChain - ).to receive(:remove).with(GpgHelpers::User1.fingerprint) - + expect(gpg_key).to receive(:synchronize_keychain) gpg_key.destroy! end end @@ -76,6 +59,15 @@ describe GpgKey do end end + describe '#emails_in_keychain', :gpg do + it 'returns the emails from the keychain' do + user = create :user, email: GpgHelpers::User1.emails.first + gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: user + + expect(gpg_key.emails_in_keychain).to eq GpgHelpers::User1.emails + end + end + describe '#emails_with_verified_status', :gpg do context 'key is in the keychain' do it 'email is verified if the user has the matching email' do @@ -104,6 +96,46 @@ describe GpgKey do end end + describe '#synchronize_keychain', :gpg do + context "user's email matches one of the key's emails" do + it 'adds the key to the keychain' do + user = create :user, email: GpgHelpers::User1.emails.first + gpg_key = create :gpg_key, user: user + + expect(gpg_key).to receive(:add_to_keychain) + + gpg_key.synchronize_keychain + end + end + + context "user's email does not match one of the key's emails" do + it 'does not add the key to the keychain' do + user = create :user, email: 'stepanie@cole.us' + gpg_key = create :gpg_key, user: user + + expect(gpg_key).to receive(:remove_from_keychain) + + gpg_key.synchronize_keychain + end + end + end + + describe '#add_to_keychain', :gpg do + it 'calls .add_to_keychain' do + expect(Gitlab::Gpg::CurrentKeyChain).to receive(:add).with(GpgHelpers::User2.public_key) + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key + gpg_key.send(:add_to_keychain) + end + end + + describe '#remove_from_keychain', :gpg do + it 'calls .remove_from_keychain' do + allow(Gitlab::Gpg::CurrentKeyChain).to receive(:remove).with(GpgHelpers::User2.fingerprint) + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key + gpg_key.send(:remove_from_keychain) + end + end + describe 'notification' do include EmailHelpers diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 20bdb7e37da..60979fd6c06 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1956,4 +1956,24 @@ describe User, models: true do expect(user.allow_password_authentication?).to be_falsey end end + + context 'callbacks' do + context '.synchronize_gpg_keys' 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(:synchronize_gpg_keys) + user.update_attributes!(name: 'Bette') + end + + it 'synchronizes the gpg keys when the email is updated' do + expect(user).to receive(:synchronize_gpg_keys) + user.update_attributes!(email: 'shawnee.ritchie@denesik.com') + end + end + end end -- cgit v1.2.1 From d1101ec02ec718d0ce15e76217980f6fa21c9089 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 12 Jun 2017 14:26:13 +0200 Subject: use more descriptive variable names --- app/models/commit.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 0d50a32d138..9c8edbb097d 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -237,13 +237,16 @@ class Commit def signature return @signature if defined?(@signature) - sig, signed = @raw.signature(project.repository) - if sig && signed - GPGME::Crypto.new.verify(sig, signed_text: signed) do |sign| - @signature = sign + @signature = nil + + signature, signed_text = @raw.signature(project.repository) + if signature && signed_text + GPGME::Crypto.new.verify(signature, signed_text: signed_text) do |verified_signature| + @signature = verified_signature end end - @signature ||= nil + + @signature end def revert_branch_name -- cgit v1.2.1 From 7e13d96715750f74db399bf40ee4ec9679bbe806 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 12 Jun 2017 16:16:33 +0200 Subject: don't sync to keychain file --- app/models/gpg_key.rb | 26 +------------ app/models/user.rb | 7 ---- lib/gitlab/gpg.rb | 18 --------- spec/lib/gitlab/gpg_spec.rb | 33 ---------------- spec/models/gpg_key_spec.rb | 95 ++++----------------------------------------- spec/models/user_spec.rb | 20 ---------- 6 files changed, 9 insertions(+), 190 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 8332ba3ee6e..d4570e36e06 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -21,9 +21,7 @@ class GpgKey < ActiveRecord::Base unless: -> { errors.has_key?(:key) } before_validation :extract_fingerprint - after_create :synchronize_keychain after_create :notify_user - after_destroy :synchronize_keychain def key=(value) value.strip! unless value.blank? @@ -34,29 +32,15 @@ class GpgKey < ActiveRecord::Base @emails ||= Gitlab::Gpg.emails_from_key(key) end - def emails_in_keychain - @emails_in_keychain ||= Gitlab::Gpg::CurrentKeyChain.emails(fingerprint) - end - def emails_with_verified_status emails.map do |email| [ email, - email == user.email && emails_in_keychain.include?(email) + email == user.email ] end end - def synchronize_keychain - if emails.include?(user.email) - add_to_keychain - else - remove_from_keychain - end - - @emails_in_keychain = nil - end - private def extract_fingerprint @@ -65,14 +49,6 @@ class GpgKey < ActiveRecord::Base self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first end - def add_to_keychain - Gitlab::Gpg::CurrentKeyChain.add(key) - end - - def remove_from_keychain - Gitlab::Gpg::CurrentKeyChain.remove(fingerprint) - end - def notify_user run_after_commit { NotificationService.new.new_gpg_key(self) } end diff --git a/app/models/user.rb b/app/models/user.rb index 42c83e3d206..5aebd36cf8a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -155,7 +155,6 @@ class User < ActiveRecord::Base before_validation :set_public_email, if: :public_email_changed? after_update :update_emails_with_primary_email, if: :email_changed? - after_update :synchronize_gpg_keys, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: :external_changed? after_save :ensure_namespace_correct @@ -1158,10 +1157,4 @@ class User < ActiveRecord::Base ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) end - - def synchronize_gpg_keys - gpg_keys.each do |gpg_key| - gpg_key.synchronize_keychain - end - end end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index ee0467ae264..384a9138fa1 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -2,24 +2,6 @@ module Gitlab module Gpg extend self - module CurrentKeyChain - extend self - - def add(key) - GPGME::Key.import(key) - end - - def remove(fingerprint) - # `#get` raises an EOFError if the keychain is empty, which is why we - # use the friendlier `#find` - GPGME::Key.find(:public, fingerprint).each(&:delete!) - end - - def emails(fingerprint) - GPGME::Key.find(:public, fingerprint).flat_map { |raw_key| raw_key.uids.map(&:email) } - end - end - def fingerprints_from_key(key) using_tmp_keychain do import = GPGME::Key.import(key) diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index c0df719c0c2..bdcf9ee0e65 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -29,36 +29,3 @@ describe Gitlab::Gpg do end end end - -describe Gitlab::Gpg::CurrentKeyChain, :gpg do - describe '.emails' do - it 'returns the emails' do - Gitlab::Gpg::CurrentKeyChain.add(GpgHelpers::User2.public_key) - - expect( - described_class.emails(GpgHelpers::User2.fingerprint) - ).to match_array GpgHelpers::User2.emails - end - end - - describe '.add', :gpg do - it 'stores the key in the keychain' do - expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq [] - - Gitlab::Gpg::CurrentKeyChain.add(GpgHelpers::User1.public_key) - - expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).not_to eq [] - end - end - - describe '.remove', :gpg do - it 'removes the key from the keychain' do - Gitlab::Gpg::CurrentKeyChain.add(GpgHelpers::User1.public_key) - expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).not_to eq [] - - Gitlab::Gpg::CurrentKeyChain.remove(GpgHelpers::User1.fingerprint) - - expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq [] - end - end -end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 18746ad9d88..6ee436b6a6d 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -21,20 +21,6 @@ describe GpgKey do expect(gpg_key.fingerprint).to eq GpgHelpers::User1.fingerprint end end - - describe 'synchronize_keychain' do - it 'calls #synchronize_keychain after create' do - gpg_key = build :gpg_key - expect(gpg_key).to receive(:synchronize_keychain) - gpg_key.save! - end - - it 'calls #remove_from_keychain after destroy' do - gpg_key = create :gpg_key - expect(gpg_key).to receive(:synchronize_keychain) - gpg_key.destroy! - end - end end describe '#key=' do @@ -59,80 +45,15 @@ describe GpgKey do end end - describe '#emails_in_keychain', :gpg do - it 'returns the emails from the keychain' do - user = create :user, email: GpgHelpers::User1.emails.first - gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: user - - expect(gpg_key.emails_in_keychain).to eq GpgHelpers::User1.emails - end - end - describe '#emails_with_verified_status', :gpg do - context 'key is in the keychain' 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 match_array [ - ['bette.cartwright@example.com', true], - ['bette.cartwright@example.net', false] - ] - end - end - - context 'key is in not the keychain' do - it 'emails are unverified' do - user = create :user, email: 'bette.cartwright@example.com' - gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user - - Gitlab::Gpg::CurrentKeyChain.remove(GpgHelpers::User2.fingerprint) - - expect(gpg_key.emails_with_verified_status).to match_array [ - ['bette.cartwright@example.com', false], - ['bette.cartwright@example.net', false] - ] - end - end - end - - describe '#synchronize_keychain', :gpg do - context "user's email matches one of the key's emails" do - it 'adds the key to the keychain' do - user = create :user, email: GpgHelpers::User1.emails.first - gpg_key = create :gpg_key, user: user - - expect(gpg_key).to receive(:add_to_keychain) - - gpg_key.synchronize_keychain - end - end - - context "user's email does not match one of the key's emails" do - it 'does not add the key to the keychain' do - user = create :user, email: 'stepanie@cole.us' - gpg_key = create :gpg_key, user: user - - expect(gpg_key).to receive(:remove_from_keychain) - - gpg_key.synchronize_keychain - end - end - end - - describe '#add_to_keychain', :gpg do - it 'calls .add_to_keychain' do - expect(Gitlab::Gpg::CurrentKeyChain).to receive(:add).with(GpgHelpers::User2.public_key) - gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key - gpg_key.send(:add_to_keychain) - end - end - - describe '#remove_from_keychain', :gpg do - it 'calls .remove_from_keychain' do - allow(Gitlab::Gpg::CurrentKeyChain).to receive(:remove).with(GpgHelpers::User2.fingerprint) - gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key - gpg_key.send(:remove_from_keychain) + 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 match_array [ + ['bette.cartwright@example.com', true], + ['bette.cartwright@example.net', false] + ] end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 60979fd6c06..20bdb7e37da 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1956,24 +1956,4 @@ describe User, models: true do expect(user.allow_password_authentication?).to be_falsey end end - - context 'callbacks' do - context '.synchronize_gpg_keys' 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(:synchronize_gpg_keys) - user.update_attributes!(name: 'Bette') - end - - it 'synchronizes the gpg keys when the email is updated' do - expect(user).to receive(:synchronize_gpg_keys) - user.update_attributes!(email: 'shawnee.ritchie@denesik.com') - end - end - end end -- cgit v1.2.1 From 3c42d730986222d891c9b7985edf3942021afcef Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 13 Jun 2017 13:46:43 +0200 Subject: add primary keyid attribute to gpg keys --- app/models/gpg_key.rb | 15 ++++++++++++++- .../20170613103429_add_primary_keyid_to_gpg_keys.rb | 17 +++++++++++++++++ db/schema.rb | 2 ++ lib/gitlab/gpg.rb | 12 ++++++++++++ spec/features/commits_spec.rb | 4 ++-- spec/lib/gitlab/gpg_spec.rb | 14 ++++++++++++++ spec/models/gpg_key_spec.rb | 8 ++++++++ spec/support/gpg_helpers.rb | 8 ++++---- 8 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20170613103429_add_primary_keyid_to_gpg_keys.rb diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index d4570e36e06..26f9a3975c9 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -20,7 +20,14 @@ class GpgKey < ActiveRecord::Base # the error about the fingerprint unless: -> { errors.has_key?(:key) } - before_validation :extract_fingerprint + 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_create :notify_user def key=(value) @@ -49,6 +56,12 @@ class GpgKey < ActiveRecord::Base 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 run_after_commit { NotificationService.new.new_gpg_key(self) } end diff --git a/db/migrate/20170613103429_add_primary_keyid_to_gpg_keys.rb b/db/migrate/20170613103429_add_primary_keyid_to_gpg_keys.rb new file mode 100644 index 00000000000..13f0500971b --- /dev/null +++ b/db/migrate/20170613103429_add_primary_keyid_to_gpg_keys.rb @@ -0,0 +1,17 @@ +class AddPrimaryKeyidToGpgKeys < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column :gpg_keys, :primary_keyid, :string + add_concurrent_index :gpg_keys, :primary_keyid + end + + def down + remove_concurrent_index :gpg_keys, :primary_keyid if index_exists?(:gpg_keys, :primary_keyid) + remove_column :gpg_keys, :primary_keyid, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 54f98559243..fbf20f4eb66 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -546,8 +546,10 @@ ActiveRecord::Schema.define(version: 20170725145659) do t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "primary_keyid" end + add_index "gpg_keys", ["primary_keyid"], name: "index_gpg_keys_on_primary_keyid", using: :btree add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree create_table "identities", force: :cascade do |t| diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 384a9138fa1..486e040adb6 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -12,6 +12,18 @@ module Gitlab end end + def primary_keyids_from_key(key) + using_tmp_keychain do + import = GPGME::Key.import(key) + + return [] if import.imported == 0 + + fingerprints = import.imports.map(&:fingerprint) + + GPGME::Key.find(:public, fingerprints).map { |raw_key| raw_key.primary_subkey.keyid } + end + end + def emails_from_key(key) using_tmp_keychain do import = GPGME::Key.import(key) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 79952eda2ff..1dbcf09d4a0 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -220,8 +220,8 @@ describe 'Commits' do Dir.mktmpdir do |dir| FileUtils.cd dir do `git clone --quiet #{remote_path} .` - `git commit --quiet -S#{GpgHelpers::User1.key_id} --allow-empty -m "signed commit, verified key/email"` - `git commit --quiet -S#{GpgHelpers::User2.key_id} --allow-empty -m "signed commit, unverified key/email"` + `git commit --quiet -S#{GpgHelpers::User1.primary_keyid} --allow-empty -m "signed commit, verified key/email"` + `git commit --quiet -S#{GpgHelpers::User2.primary_keyid} --allow-empty -m "signed commit, unverified key/email"` `git push --quiet` end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index bdcf9ee0e65..55f34e0cf99 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -15,6 +15,20 @@ describe Gitlab::Gpg do 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 '.emails_from_key' do it 'returns the emails' do expect( diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 6ee436b6a6d..ac446fca819 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -21,6 +21,14 @@ describe GpgKey do 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 diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb index 52c478e1976..f9128a629f2 100644 --- a/spec/support/gpg_helpers.rb +++ b/spec/support/gpg_helpers.rb @@ -90,8 +90,8 @@ module GpgHelpers KEY end - def key_id - '00AC8B1D' + def primary_keyid + fingerprint[-16..-1] end def fingerprint @@ -179,8 +179,8 @@ module GpgHelpers KEY end - def key_id - '911EFD65' + def primary_keyid + fingerprint[-16..-1] end def fingerprint -- cgit v1.2.1 From 2f956fae0399f6f2eb370ed186c7bb4a9486178b Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 13 Jun 2017 14:26:42 +0200 Subject: verify gpg commit using tmp keyring and db query --- app/models/commit.rb | 17 ++++++++++++++++- lib/gitlab/gpg.rb | 8 ++++++++ spec/lib/gitlab/gpg_spec.rb | 17 +++++++++++++++++ spec/models/commit_spec.rb | 4 ++-- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 9c8edbb097d..a6a11a2d3a5 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -240,7 +240,22 @@ class Commit @signature = nil signature, signed_text = @raw.signature(project.repository) - if signature && signed_text + + return unless signature && signed_text + + Gitlab::Gpg.using_tmp_keychain do + # first we need to get the keyid from the signature... + GPGME::Crypto.new.verify(signature, signed_text: signed_text) do |verified_signature| + @signature = verified_signature + end + + # ... then we query the gpg key belonging to the keyid. + gpg_key = GpgKey.find_by(primary_keyid: @signature.fingerprint) + + return @signature unless gpg_key + + Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) + GPGME::Crypto.new.verify(signature, signed_text: signed_text) do |verified_signature| @signature = verified_signature end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 486e040adb6..258901bb238 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -2,6 +2,14 @@ module Gitlab module Gpg extend self + module CurrentKeyChain + extend self + + def add(key) + GPGME::Key.import(key) + end + end + def fingerprints_from_key(key) using_tmp_keychain do import = GPGME::Key.import(key) diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 55f34e0cf99..edf7405d7f1 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -43,3 +43,20 @@ describe Gitlab::Gpg do end end end + +describe Gitlab::Gpg::CurrentKeyChain, :gpg do + describe '.add', :gpg 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 +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 3c6ce49b48d..96af675c3f4 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -422,7 +422,7 @@ eos context 'signed commit', :gpg do it 'returns a valid signature if the public key is known' do - GPGME::Key.import(GpgHelpers::User1.public_key) + create :gpg_key, key: GpgHelpers::User1.public_key raw_commit = double(:raw_commit, signature: [ GpgHelpers::User1.signed_commit_signature, @@ -438,7 +438,7 @@ eos expect(commit.signature.valid?).to be_truthy end - it 'returns an invalid signature if the public commit is unknown', :gpg do + it 'returns an invalid signature if the public key is unknown', :gpg do raw_commit = double(:raw_commit, signature: [ GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data -- cgit v1.2.1 From 8236b12dff3df6d223888664c820ae54b4e0eaf7 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 14 Jun 2017 09:17:34 +0200 Subject: gpg signature model for gpg verification caching --- app/models/gpg_signature.rb | 7 ++++++ db/migrate/20170613154149_create_gpg_signatures.rb | 29 ++++++++++++++++++++++ db/schema.rb | 17 +++++++++++++ spec/models/gpg_signature_spec.rb | 14 +++++++++++ 4 files changed, 67 insertions(+) create mode 100644 app/models/gpg_signature.rb create mode 100644 db/migrate/20170613154149_create_gpg_signatures.rb create mode 100644 spec/models/gpg_signature_spec.rb diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb new file mode 100644 index 00000000000..03294354d91 --- /dev/null +++ b/app/models/gpg_signature.rb @@ -0,0 +1,7 @@ +class GpgSignature < ActiveRecord::Base + belongs_to :project + belongs_to :gpg_key + + validates :commit_sha, presence: true + validates :project, presence: true +end diff --git a/db/migrate/20170613154149_create_gpg_signatures.rb b/db/migrate/20170613154149_create_gpg_signatures.rb new file mode 100644 index 00000000000..72560cdb6d0 --- /dev/null +++ b/db/migrate/20170613154149_create_gpg_signatures.rb @@ -0,0 +1,29 @@ +class CreateGpgSignatures < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :gpg_signatures do |t| + t.string :commit_sha + t.references :project, index: true, foreign_key: true + t.references :gpg_key, index: true, foreign_key: true + t.string :gpg_key_primary_keyid + t.boolean :valid_signature + + t.timestamps_with_timezone null: false + end + + add_concurrent_index :gpg_signatures, :commit_sha + add_concurrent_index :gpg_signatures, :gpg_key_primary_keyid + end + + def down + remove_concurrent_index :gpg_signatures, :commit_sha if index_exists?(:gpg_signatures, :commit_sha) + remove_concurrent_index :gpg_signatures, :gpg_key_primary_keyid if index_exists?(:gpg_signatures, :gpg_key_primary_keyid) + + drop_table :gpg_signatures + end +end diff --git a/db/schema.rb b/db/schema.rb index fbf20f4eb66..53b1e83ddab 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -552,6 +552,21 @@ ActiveRecord::Schema.define(version: 20170725145659) do add_index "gpg_keys", ["primary_keyid"], name: "index_gpg_keys_on_primary_keyid", 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.string "commit_sha" + t.integer "project_id" + t.integer "gpg_key_id" + t.string "gpg_key_primary_keyid" + t.boolean "valid_signature" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", 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" @@ -1615,6 +1630,8 @@ ActiveRecord::Schema.define(version: 20170725145659) do 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" + add_foreign_key "gpg_signatures", "gpg_keys" + add_foreign_key "gpg_signatures", "projects" 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 diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb new file mode 100644 index 00000000000..d2720c41694 --- /dev/null +++ b/spec/models/gpg_signature_spec.rb @@ -0,0 +1,14 @@ +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) } + end +end -- cgit v1.2.1 From 69e511c4c2a0409fa69658cf95bf5c4072b2b2d0 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 14 Jun 2017 11:51:34 +0200 Subject: cache the gpg commit signature we store the result of the gpg commit verification in the db because the gpg verification is an expensive operation. --- app/models/commit.rb | 25 ++------ app/views/projects/commit/_signature.html.haml | 6 +- lib/gitlab/gpg/commit.rb | 51 +++++++++++++++ spec/lib/gitlab/gpg/commit_spec.rb | 53 ++++++++++++++++ spec/models/commit_spec.rb | 88 +++++++++++++++++++------- 5 files changed, 177 insertions(+), 46 deletions(-) create mode 100644 lib/gitlab/gpg/commit.rb create mode 100644 spec/lib/gitlab/gpg/commit_spec.rb diff --git a/app/models/commit.rb b/app/models/commit.rb index a6a11a2d3a5..6c5556902ec 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -239,29 +239,14 @@ class Commit @signature = nil - signature, signed_text = @raw.signature(project.repository) + cached_signature = GpgSignature.find_by(commit_sha: sha) + return cached_signature if cached_signature.present? - return unless signature && signed_text + gpg_commit = Gitlab::Gpg::Commit.new(self) - Gitlab::Gpg.using_tmp_keychain do - # first we need to get the keyid from the signature... - GPGME::Crypto.new.verify(signature, signed_text: signed_text) do |verified_signature| - @signature = verified_signature - end - - # ... then we query the gpg key belonging to the keyid. - gpg_key = GpgKey.find_by(primary_keyid: @signature.fingerprint) - - return @signature unless gpg_key - - Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) - - GPGME::Crypto.new.verify(signature, signed_text: signed_text) do |verified_signature| - @signature = verified_signature - end - end + return unless gpg_commit.has_signature? - @signature + @signature = gpg_commit.signature end def revert_branch_name diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml index 7335b6b9597..48665ede6eb 100644 --- a/app/views/projects/commit/_signature.html.haml +++ b/app/views/projects/commit/_signature.html.haml @@ -1,4 +1,4 @@ - if signature - %a.btn.disabled.btn-xs{ class: ('btn-success' if signature.valid?) } - %i.fa.fa-key{ class: ('fa-inverse' if signature.valid?) } - = signature.valid? ? 'Verified': 'Unverified' + %a.btn.disabled.btn-xs{ class: ('btn-success' if signature.valid_signature?) } + %i.fa.fa-key{ class: ('fa-inverse' if signature.valid_signature?) } + = signature.valid_signature? ? 'Verified' : 'Unverified' diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb new file mode 100644 index 00000000000..f60e5125c13 --- /dev/null +++ b/lib/gitlab/gpg/commit.rb @@ -0,0 +1,51 @@ +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 + 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) + end + + create_cached_signature!(gpg_key) + end + end + + private + + def verified_signature + GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature| + return verified_signature + end + end + + def create_cached_signature!(gpg_key) + GpgSignature.create!( + commit_sha: commit.sha, + project: commit.project, + gpg_key: gpg_key, + gpg_key_primary_keyid: gpg_key&.primary_keyid, + valid_signature: !!(gpg_key && verified_signature&.valid?) + ) + end + end + end +end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb new file mode 100644 index 00000000000..8b1747eebcc --- /dev/null +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe Gitlab::Gpg::Commit do + describe '#signature' do + let!(:project) { create :project, :repository, path: 'sample-project' } + + context 'known public key' do + it 'returns a valid signature' do + gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key + + raw_commit = double(:raw_commit, signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + allow(raw_commit).to receive :save! + + commit = create :commit, + git_commit: raw_commit, + project: project + + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + valid_signature: true + ) + end + end + + context 'unknown public key' do + it 'returns an invalid signature', :gpg do + raw_commit = double(:raw_commit, signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + allow(raw_commit).to receive :save! + + commit = create :commit, + git_commit: raw_commit, + project: project + + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', + project: project, + gpg_key: nil, + gpg_key_primary_keyid: nil, + valid_signature: false + ) + end + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 96af675c3f4..4370c78e6fd 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -421,36 +421,78 @@ eos end context 'signed commit', :gpg do - it 'returns a valid signature if the public key is known' do - create :gpg_key, key: GpgHelpers::User1.public_key + context 'known public key' do + it 'returns a valid signature' do + create :gpg_key, key: GpgHelpers::User1.public_key - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ]) - allow(raw_commit).to receive :save! + raw_commit = double(:raw_commit, signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + allow(raw_commit).to receive :save! - commit = create :commit, - git_commit: raw_commit, - project: project + commit = create :commit, + git_commit: raw_commit, + project: project - expect(commit.signature).to be_a GPGME::Signature - expect(commit.signature.valid?).to be_truthy + expect(commit.signature.valid_signature?).to be_truthy + end + + it 'returns the cached validation result on second call', :gpg do + create :gpg_key, key: GpgHelpers::User1.public_key + + raw_commit = double(:raw_commit, signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + allow(raw_commit).to receive :save! + + commit = create :commit, + git_commit: raw_commit, + project: project + + expect(Gitlab::Gpg::Commit).to receive(:new).and_call_original + expect(commit.signature.valid_signature?).to be_truthy + + # second call returns the cache + expect(Gitlab::Gpg::Commit).not_to receive(:new).and_call_original + expect(commit.signature.valid_signature?).to be_truthy + end end - it 'returns an invalid signature if the public key is unknown', :gpg do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ]) - allow(raw_commit).to receive :save! + context 'unknown public key' do + it 'returns an invalid signature if the public key is unknown', :gpg do + raw_commit = double(:raw_commit, signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + allow(raw_commit).to receive :save! - commit = create :commit, - git_commit: raw_commit, - project: project + commit = create :commit, + git_commit: raw_commit, + project: project - expect(commit.signature).to be_a GPGME::Signature - expect(commit.signature.valid?).to be_falsey + expect(commit.signature.valid_signature?).to be_falsey + end + + it 'returns the cached validation result on second call', :gpg do + raw_commit = double(:raw_commit, signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + allow(raw_commit).to receive :save! + + commit = create :commit, + git_commit: raw_commit, + project: project + + expect(Gitlab::Gpg::Commit).to receive(:new).and_call_original + expect(commit.signature.valid_signature?).to be_falsey + + # second call returns the cache + expect(Gitlab::Gpg::Commit).not_to receive(:new).and_call_original + expect(commit.signature.valid_signature?).to be_falsey + end end end end -- cgit v1.2.1 From 8c4b6a32fcc5786383904fa1d5cf8b317bec7a7f Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 15 Jun 2017 09:16:50 +0200 Subject: bail if the commit has no signature --- app/models/commit.rb | 6 +----- lib/gitlab/gpg/commit.rb | 6 ++++-- spec/lib/gitlab/gpg/commit_spec.rb | 6 ++++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 6c5556902ec..ed8b9a79a7a 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -242,11 +242,7 @@ class Commit cached_signature = GpgSignature.find_by(commit_sha: sha) return cached_signature if cached_signature.present? - gpg_commit = Gitlab::Gpg::Commit.new(self) - - return unless gpg_commit.has_signature? - - @signature = gpg_commit.signature + @signature = Gitlab::Gpg::Commit.new(self).signature end def revert_branch_name diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index f60e5125c13..f363652745f 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -10,10 +10,12 @@ module Gitlab end def has_signature? - @signature_text && @signed_text + !!(@signature_text && @signed_text) end def signature + return unless has_signature? + 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. @@ -43,7 +45,7 @@ module Gitlab project: commit.project, gpg_key: gpg_key, gpg_key_primary_keyid: gpg_key&.primary_keyid, - valid_signature: !!(gpg_key && verified_signature&.valid?) + valid_signature: !!(gpg_key && verified_signature.valid?) ) end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 8b1747eebcc..c4d92b8bbbf 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -4,6 +4,12 @@ RSpec.describe Gitlab::Gpg::Commit do describe '#signature' do let!(:project) { create :project, :repository, path: 'sample-project' } + context 'unisgned commit' do + it 'returns nil' do + expect(described_class.new(project.commit).signature).to be_nil + end + end + context 'known public key' do it 'returns a valid signature' do gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key -- cgit v1.2.1 From 7b616d39efaa7cba933d17dfae010d393c18d057 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 15 Jun 2017 09:57:50 +0200 Subject: gpg signature is only valid when key is verified --- app/models/gpg_key.rb | 4 ++++ lib/gitlab/gpg/commit.rb | 2 +- spec/lib/gitlab/gpg/commit_spec.rb | 28 ++++++++++++++++++++++++++-- spec/models/gpg_key_spec.rb | 16 ++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 26f9a3975c9..137abb60ddc 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -48,6 +48,10 @@ class GpgKey < ActiveRecord::Base end end + def verified? + emails_with_verified_status.any? { |_email, verified| verified } + end + private def extract_fingerprint diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index f363652745f..d65a20f08f9 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -45,7 +45,7 @@ module Gitlab project: commit.project, gpg_key: gpg_key, gpg_key_primary_keyid: gpg_key&.primary_keyid, - valid_signature: !!(gpg_key && verified_signature.valid?) + valid_signature: !!(gpg_key && gpg_key.verified? && verified_signature.valid?) ) end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index c4d92b8bbbf..2a583dc1bd5 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -10,9 +10,9 @@ RSpec.describe Gitlab::Gpg::Commit do end end - context 'known public key' do + context 'known and verified public key' do it 'returns a valid signature' do - gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key + gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first) raw_commit = double(:raw_commit, signature: [ GpgHelpers::User1.signed_commit_signature, @@ -34,6 +34,30 @@ RSpec.describe Gitlab::Gpg::Commit do end end + context 'known but unverified public key' do + it 'returns an invalid signature' do + gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key + + raw_commit = double(:raw_commit, signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + allow(raw_commit).to receive :save! + + commit = create :commit, + git_commit: raw_commit, + project: project + + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + valid_signature: false + ) + end + end + context 'unknown public key' do it 'returns an invalid signature', :gpg do raw_commit = double(:raw_commit, signature: [ diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index ac446fca819..3cb1723cc12 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -65,6 +65,22 @@ describe GpgKey do 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 -- cgit v1.2.1 From 34810acd6c3d4dd27f43f6f07e47b4e06bb95f82 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 15 Jun 2017 10:28:28 +0200 Subject: move signature cache read to Gpg::Commit as we write the cache in the gpg commit class already the read should also happen there. This also removes all logic from the main commit class, which just proxies the call to the Gpg::Commit now. --- app/models/commit.rb | 5 --- lib/gitlab/gpg/commit.rb | 3 ++ spec/lib/gitlab/gpg/commit_spec.rb | 61 ++++++++++++++++++++++------ spec/models/commit_spec.rb | 82 -------------------------------------- 4 files changed, 52 insertions(+), 99 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index ed8b9a79a7a..35593d53cbc 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -237,11 +237,6 @@ class Commit def signature return @signature if defined?(@signature) - @signature = nil - - cached_signature = GpgSignature.find_by(commit_sha: sha) - return cached_signature if cached_signature.present? - @signature = Gitlab::Gpg::Commit.new(self).signature end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index d65a20f08f9..2b61caaebb5 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -16,6 +16,9 @@ module Gitlab def signature return unless has_signature? + cached_signature = GpgSignature.find_by(commit_sha: commit.sha) + return cached_signature if cached_signature.present? + 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. diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 2a583dc1bd5..539e6d4641f 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -11,19 +11,21 @@ RSpec.describe Gitlab::Gpg::Commit do end context 'known and verified public key' do - it 'returns a valid signature' do - gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first) + 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: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') allow(raw_commit).to receive :save! - commit = create :commit, - git_commit: raw_commit, - project: project + 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: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', project: project, @@ -32,22 +34,33 @@ RSpec.describe Gitlab::Gpg::Commit do valid_signature: true ) end + + it 'returns the cached signature on second call' do + gpg_commit = described_class.new(commit) + + expect(gpg_commit).to receive(:verified_signature).twice.and_call_original + gpg_commit.signature + + # consecutive call + expect(gpg_commit).not_to receive(:verified_signature).and_call_original + gpg_commit.signature + end end context 'known but unverified public key' do - it 'returns an invalid signature' do - gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key + 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: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') allow(raw_commit).to receive :save! - commit = create :commit, - git_commit: raw_commit, - project: project + 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: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', project: project, @@ -56,20 +69,33 @@ RSpec.describe Gitlab::Gpg::Commit do valid_signature: false ) end + + it 'returns the cached signature on second call' do + gpg_commit = described_class.new(commit) + + expect(gpg_commit).to receive(:verified_signature).and_call_original + gpg_commit.signature + + # consecutive call + expect(gpg_commit).not_to receive(:verified_signature).and_call_original + gpg_commit.signature + end end context 'unknown public key' do - it 'returns an invalid signature', :gpg do + let!(:commit) do raw_commit = double(:raw_commit, signature: [ GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') allow(raw_commit).to receive :save! - commit = create :commit, + 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: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', project: project, @@ -78,6 +104,17 @@ RSpec.describe Gitlab::Gpg::Commit do valid_signature: false ) end + + it 'returns the cached signature on second call' do + gpg_commit = described_class.new(commit) + + expect(gpg_commit).to receive(:verified_signature).and_call_original + gpg_commit.signature + + # consecutive call + expect(gpg_commit).not_to receive(:verified_signature).and_call_original + gpg_commit.signature + end end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 4370c78e6fd..528b211c9d6 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -414,86 +414,4 @@ eos expect(described_class.valid_hash?('a' * 41)).to be false end end - - describe '#signature' do - it 'returns nil if the commit is not signed' do - expect(commit.signature).to be_nil - end - - context 'signed commit', :gpg do - context 'known public key' do - it 'returns a valid signature' do - create :gpg_key, key: GpgHelpers::User1.public_key - - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') - allow(raw_commit).to receive :save! - - commit = create :commit, - git_commit: raw_commit, - project: project - - expect(commit.signature.valid_signature?).to be_truthy - end - - it 'returns the cached validation result on second call', :gpg do - create :gpg_key, key: GpgHelpers::User1.public_key - - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') - allow(raw_commit).to receive :save! - - commit = create :commit, - git_commit: raw_commit, - project: project - - expect(Gitlab::Gpg::Commit).to receive(:new).and_call_original - expect(commit.signature.valid_signature?).to be_truthy - - # second call returns the cache - expect(Gitlab::Gpg::Commit).not_to receive(:new).and_call_original - expect(commit.signature.valid_signature?).to be_truthy - end - end - - context 'unknown public key' do - it 'returns an invalid signature if the public key is unknown', :gpg do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') - allow(raw_commit).to receive :save! - - commit = create :commit, - git_commit: raw_commit, - project: project - - expect(commit.signature.valid_signature?).to be_falsey - end - - it 'returns the cached validation result on second call', :gpg do - raw_commit = double(:raw_commit, signature: [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') - allow(raw_commit).to receive :save! - - commit = create :commit, - git_commit: raw_commit, - project: project - - expect(Gitlab::Gpg::Commit).to receive(:new).and_call_original - expect(commit.signature.valid_signature?).to be_falsey - - # second call returns the cache - expect(Gitlab::Gpg::Commit).not_to receive(:new).and_call_original - expect(commit.signature.valid_signature?).to be_falsey - end - end - end - end end -- cgit v1.2.1 From 5d5fd4babe4cb75c7f8f9f18cc86c63a0fa58d16 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 15 Jun 2017 12:43:04 +0200 Subject: store gpg_key_primary_keyid for unknown gpg keys we need to store the keyid to be able to update the signature later in case the missing key is added later. --- app/models/gpg_signature.rb | 1 + lib/gitlab/gpg/commit.rb | 6 ++++-- spec/lib/gitlab/gpg/commit_spec.rb | 2 +- spec/models/gpg_signature_spec.rb | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 03294354d91..4fe9c3210ff 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -4,4 +4,5 @@ class GpgSignature < ActiveRecord::Base validates :commit_sha, presence: true validates :project, presence: true + validates :gpg_key_primary_keyid, presence: true end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 2b61caaebb5..8d2e6269618 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -43,12 +43,14 @@ module Gitlab end def create_cached_signature!(gpg_key) + verified_signature_result = verified_signature + GpgSignature.create!( commit_sha: commit.sha, project: commit.project, gpg_key: gpg_key, - gpg_key_primary_keyid: gpg_key&.primary_keyid, - valid_signature: !!(gpg_key && gpg_key.verified? && verified_signature.valid?) + gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature_result.fingerprint, + valid_signature: !!(gpg_key && gpg_key.verified? && verified_signature_result.valid?) ) end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 539e6d4641f..448b16a656e 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -100,7 +100,7 @@ RSpec.describe Gitlab::Gpg::Commit do commit_sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', project: project, gpg_key: nil, - gpg_key_primary_keyid: nil, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, valid_signature: false ) end diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb index d2720c41694..b3f84262874 100644 --- a/spec/models/gpg_signature_spec.rb +++ b/spec/models/gpg_signature_spec.rb @@ -10,5 +10,6 @@ RSpec.describe GpgSignature do subject { described_class.new } it { is_expected.to validate_presence_of(:commit_sha) } it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:gpg_key_primary_keyid) } end end -- cgit v1.2.1 From 502e31bec9af080bcb483b0d57c8b52aeb507f93 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 15 Jun 2017 13:37:03 +0200 Subject: memoize verified_signature call --- lib/gitlab/gpg/commit.rb | 25 +++++++++++++++++-------- spec/lib/gitlab/gpg/commit_spec.rb | 12 ++++++------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 8d2e6269618..8bc430db715 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -19,6 +19,19 @@ module Gitlab cached_signature = GpgSignature.find_by(commit_sha: commit.sha) return cached_signature if cached_signature.present? + using_keychain do |gpg_key| + if gpg_key + Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) + @verified_signature = nil + end + + create_cached_signature!(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. @@ -30,27 +43,23 @@ module Gitlab Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) end - create_cached_signature!(gpg_key) + yield gpg_key end end - private - def verified_signature - GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature| + @verified_signature ||= GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature| return verified_signature end end def create_cached_signature!(gpg_key) - verified_signature_result = verified_signature - GpgSignature.create!( commit_sha: commit.sha, project: commit.project, gpg_key: gpg_key, - gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature_result.fingerprint, - valid_signature: !!(gpg_key && gpg_key.verified? && verified_signature_result.valid?) + gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, + valid_signature: !!(gpg_key && gpg_key.verified? && verified_signature.valid?) ) end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 448b16a656e..387ce8f74b4 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -38,11 +38,11 @@ RSpec.describe Gitlab::Gpg::Commit do it 'returns the cached signature on second call' do gpg_commit = described_class.new(commit) - expect(gpg_commit).to receive(:verified_signature).twice.and_call_original + expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature # consecutive call - expect(gpg_commit).not_to receive(:verified_signature).and_call_original + expect(gpg_commit).not_to receive(:using_keychain).and_call_original gpg_commit.signature end end @@ -73,11 +73,11 @@ RSpec.describe Gitlab::Gpg::Commit do it 'returns the cached signature on second call' do gpg_commit = described_class.new(commit) - expect(gpg_commit).to receive(:verified_signature).and_call_original + expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature # consecutive call - expect(gpg_commit).not_to receive(:verified_signature).and_call_original + expect(gpg_commit).not_to receive(:using_keychain).and_call_original gpg_commit.signature end end @@ -108,11 +108,11 @@ RSpec.describe Gitlab::Gpg::Commit do it 'returns the cached signature on second call' do gpg_commit = described_class.new(commit) - expect(gpg_commit).to receive(:verified_signature).and_call_original + expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature # consecutive call - expect(gpg_commit).not_to receive(:verified_signature).and_call_original + expect(gpg_commit).not_to receive(:using_keychain).and_call_original gpg_commit.signature end end -- cgit v1.2.1 From d48eb77a96d29260c214391c5b8979ee17250452 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 15 Jun 2017 14:18:00 +0200 Subject: allow updating of gpg signature through gpg commit --- lib/gitlab/gpg/commit.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 8bc430db715..6e3f7c28eba 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -29,6 +29,14 @@ module Gitlab end end + def update_signature!(cached_signature) + using_keychain do |gpg_key| + cached_signature.update_attributes!( + valid_signature: self.class.gpg_signature_valid_signature_value(gpg_key, verified_signature) + ) + end + end + private def using_keychain @@ -59,9 +67,13 @@ module Gitlab project: commit.project, gpg_key: gpg_key, gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, - valid_signature: !!(gpg_key && gpg_key.verified? && verified_signature.valid?) + valid_signature: self.class.gpg_signature_valid_signature_value(gpg_key, verified_signature) ) end + + def self.gpg_signature_valid_signature_value(gpg_key, verified_signature_) + !!(gpg_key && gpg_key.verified? && verified_signature_.valid?) + end end end end -- cgit v1.2.1 From 24671cd601e93133787ff9746fcacc3cf5d3fbf4 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 15 Jun 2017 14:22:37 +0200 Subject: update invalid gpg signatures when key is created --- app/models/gpg_key.rb | 5 +++ lib/gitlab/gpg/invalid_gpg_signature_updater.rb | 19 ++++++++ spec/factories/gpg_signature.rb | 11 +++++ .../gpg/invalid_gpg_signature_updater_spec.rb | 50 ++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 lib/gitlab/gpg/invalid_gpg_signature_updater.rb create mode 100644 spec/factories/gpg_signature.rb create mode 100644 spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 137abb60ddc..6ca108d6b87 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -28,6 +28,7 @@ class GpgKey < ActiveRecord::Base unless: -> { errors.has_key?(:key) } before_validation :extract_fingerprint, :extract_primary_keyid + after_create :update_invalid_gpg_signatures after_create :notify_user def key=(value) @@ -66,6 +67,10 @@ class GpgKey < ActiveRecord::Base self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first end + def update_invalid_gpg_signatures + run_after_commit { Gitlab::Gpg::InvalidGpgSignatureUpdater.new(self).run } + end + def notify_user run_after_commit { NotificationService.new.new_gpg_key(self) } end diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb new file mode 100644 index 00000000000..6511a8f8285 --- /dev/null +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -0,0 +1,19 @@ +module Gitlab + module Gpg + class InvalidGpgSignatureUpdater + def initialize(gpg_key) + @gpg_key = gpg_key + end + + def run + GpgSignature + .where(valid_signature: false) + .where(gpg_key_primary_keyid: @gpg_key.primary_keyid) + .find_each do |gpg_signature| + commit = Gitlab::Git::Commit.find(gpg_signature.project.repository, gpg_signature.commit_sha) + Gitlab::Gpg::Commit.new(commit).update_signature!(gpg_signature) + end + end + end + end +end diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb new file mode 100644 index 00000000000..a5aeffbe12d --- /dev/null +++ b/spec/factories/gpg_signature.rb @@ -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 diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb new file mode 100644 index 00000000000..48f8fa285aa --- /dev/null +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do + describe '#run' do + context 'gpg signature did not have an associated gpg key' do + let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } + let!(:project) { create :project, :repository, path: 'sample-project' } + 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 + + let!(: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 + + before do + allow(Gitlab::Git::Commit).to receive(:find).with(kind_of(Repository), commit_sha).and_return(commit) + end + + it 'updates the signature to being valid when the missing gpg key is added' do + # InvalidGpgSignatureUpdater is called by the after_create hook + create :gpg_key, + key: GpgHelpers::User1.public_key, + user: create(:user, email: GpgHelpers::User1.emails.first) + + expect(gpg_signature.reload.valid_signature).to be_truthy + 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: create(:user, email: GpgHelpers::User2.emails.first) + + expect(gpg_signature.reload.valid_signature).to be_falsey + end + end + end +end -- cgit v1.2.1 From e75ab064302bcec45a5953a636cc9f3295f2690c Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 15 Jun 2017 15:07:44 +0200 Subject: update invalid gpg signatures when email changes --- app/models/gpg_key.rb | 8 +- app/models/user.rb | 5 ++ .../gpg/invalid_gpg_signature_updater_spec.rb | 86 +++++++++++++++------- spec/models/user_spec.rb | 20 +++++ 4 files changed, 90 insertions(+), 29 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 6ca108d6b87..ec30658e7ea 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -53,6 +53,10 @@ class GpgKey < ActiveRecord::Base emails_with_verified_status.any? { |_email, verified| verified } end + def update_invalid_gpg_signatures + Gitlab::Gpg::InvalidGpgSignatureUpdater.new(self).run + end + private def extract_fingerprint @@ -67,10 +71,6 @@ class GpgKey < ActiveRecord::Base self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first end - def update_invalid_gpg_signatures - run_after_commit { Gitlab::Gpg::InvalidGpgSignatureUpdater.new(self).run } - end - def notify_user run_after_commit { NotificationService.new.new_gpg_key(self) } end diff --git a/app/models/user.rb b/app/models/user.rb index 5aebd36cf8a..791d099605d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -155,6 +155,7 @@ class User < ActiveRecord::Base before_validation :set_public_email, if: :public_email_changed? after_update :update_emails_with_primary_email, if: :email_changed? + after_update :update_invalid_gpg_signatures, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: :external_changed? after_save :ensure_namespace_correct @@ -513,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 diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index 48f8fa285aa..42348b3f2c1 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -2,37 +2,39 @@ require 'rails_helper' RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do describe '#run' do - context 'gpg signature did not have an associated gpg key' do - let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - let!(:project) { create :project, :repository, path: 'sample-project' } - 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 + let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } + let!(:project) { create :project, :repository, path: 'sample-project' } + 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! - let!(: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 + create :commit, git_commit: raw_commit, project: project + end - before do - allow(Gitlab::Git::Commit).to receive(:find).with(kind_of(Repository), commit_sha).and_return(commit) - end + let!(: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 + + before do + allow(Gitlab::Git::Commit).to receive(:find).with(kind_of(Repository), commit_sha).and_return(commit) + end + + context 'gpg signature did not have an associated gpg key' do + let!(:user) { create :user, email: GpgHelpers::User1.emails.first } it 'updates the signature to being valid when the missing gpg key is added' do # InvalidGpgSignatureUpdater is called by the after_create hook create :gpg_key, key: GpgHelpers::User1.public_key, - user: create(:user, email: GpgHelpers::User1.emails.first) + user: user expect(gpg_signature.reload.valid_signature).to be_truthy end @@ -41,7 +43,41 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do # InvalidGpgSignatureUpdater is called by the after_create hook create :gpg_key, key: GpgHelpers::User2.public_key, - user: create(:user, email: GpgHelpers::User2.emails.first) + user: user + + expect(gpg_signature.reload.valid_signature).to be_falsey + 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 + + it 'updates the signature to being valid when the user updates the email address' do + create :gpg_key, + key: GpgHelpers::User1.public_key, + user: user + + expect(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(gpg_signature.reload.valid_signature).to be_truthy + end + + it 'keeps the signature at being invalid when the changed email address is still unrelated' do + create :gpg_key, + key: GpgHelpers::User1.public_key, + user: user + + expect(gpg_signature.reload.valid_signature).to be_falsey + + # InvalidGpgSignatureUpdater is called by the after_update hook + user.update_attributes!(email: 'still.unrelated@example.com') expect(gpg_signature.reload.valid_signature).to be_falsey end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 20bdb7e37da..14b0440af9c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -350,6 +350,26 @@ describe User, models: true 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) } -- cgit v1.2.1 From d7f42643681ad1cc634a502ef25acab6b111c6e5 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 16 Jun 2017 14:37:28 +0200 Subject: no need for passing parameter we introduced memoizing, so it's safe to call the method multiple times. --- lib/gitlab/gpg/commit.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 6e3f7c28eba..437f13ef311 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -32,7 +32,7 @@ module Gitlab def update_signature!(cached_signature) using_keychain do |gpg_key| cached_signature.update_attributes!( - valid_signature: self.class.gpg_signature_valid_signature_value(gpg_key, verified_signature) + valid_signature: gpg_signature_valid_signature_value(gpg_key) ) end end @@ -67,12 +67,12 @@ module Gitlab project: commit.project, gpg_key: gpg_key, gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, - valid_signature: self.class.gpg_signature_valid_signature_value(gpg_key, verified_signature) + valid_signature: gpg_signature_valid_signature_value(gpg_key) ) end - def self.gpg_signature_valid_signature_value(gpg_key, verified_signature_) - !!(gpg_key && gpg_key.verified? && verified_signature_.valid?) + def gpg_signature_valid_signature_value(gpg_key) + !!(gpg_key && gpg_key.verified? && verified_signature.valid?) end end end -- cgit v1.2.1 From 028ecb081b7ed71d5123ded535d5b7f87db7cc67 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 16 Jun 2017 14:43:01 +0200 Subject: need to wrap the raw commit in a commit model --- lib/gitlab/gpg/invalid_gpg_signature_updater.rb | 3 ++- spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index 6511a8f8285..06e4823de32 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -10,7 +10,8 @@ module Gitlab .where(valid_signature: false) .where(gpg_key_primary_keyid: @gpg_key.primary_keyid) .find_each do |gpg_signature| - commit = Gitlab::Git::Commit.find(gpg_signature.project.repository, gpg_signature.commit_sha) + raw_commit = Gitlab::Git::Commit.find(gpg_signature.project.repository, gpg_signature.commit_sha) + commit = ::Commit.new(raw_commit, gpg_signature.project) Gitlab::Gpg::Commit.new(commit).update_signature!(gpg_signature) end end diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index 42348b3f2c1..8b60b36452b 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -4,13 +4,18 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do describe '#run' do let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } let!(:project) { create :project, :repository, path: 'sample-project' } - let!(:commit) do + 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 @@ -24,7 +29,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do end before do - allow(Gitlab::Git::Commit).to receive(:find).with(kind_of(Repository), commit_sha).and_return(commit) + allow(Gitlab::Git::Commit).to receive(:find).with(kind_of(Repository), commit_sha).and_return(raw_commit) end context 'gpg signature did not have an associated gpg key' do -- cgit v1.2.1 From a01eabc19f0d5b73f6216edc10d19a7765c73b53 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 21 Jun 2017 21:54:16 +0200 Subject: update rugged the rugged versions up to 0.26.0b3 had a bug concerning the signature extraction. The extracted signature was not always the same, probably due to a buffer (overflow) issue in libgit. see https://github.com/libgit2/rugged/issues/608 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- lib/gitlab/git/commit.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 93934d03e42..d41d75b250a 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index 77e87e2885f..2483b0bf35c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -751,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) @@ -1084,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) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 4dcaff5e0a0..ca7e3a7c4be 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -336,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 -- cgit v1.2.1 From 9d30a80d24a583aad267a8a11f685058eab2c864 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 22 Jun 2017 08:47:11 +0200 Subject: update features specs for gpg commits --- spec/features/commits_spec.rb | 51 ++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 1dbcf09d4a0..8f89b465160 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -206,36 +206,51 @@ describe 'Commits' do end describe 'GPG signed commits' do - let!(:user) { create :user, email: GpgHelpers::User1.emails.first } - let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key, user: user } - before do - project.team << [user, :master] - login_with(user) - end - - it 'shows the signed status', :gpg do # FIXME: add this to the test repository directly remote_path = project.repository.path_to_repo Dir.mktmpdir do |dir| FileUtils.cd dir do `git clone --quiet #{remote_path} .` - `git commit --quiet -S#{GpgHelpers::User1.primary_keyid} --allow-empty -m "signed commit, verified key/email"` - `git commit --quiet -S#{GpgHelpers::User2.primary_keyid} --allow-empty -m "signed commit, unverified key/email"` + `git commit --quiet -S#{GpgHelpers::User1.primary_keyid} --allow-empty -m "signed commit by nannie bernhard"` + `git commit --quiet -S#{GpgHelpers::User2.primary_keyid} --allow-empty -m "signed commit by bette cartwright"` `git push --quiet` end end + end + + 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] + + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + + login_with(user) visit namespace_project_commits_path(project.namespace, project, :master) within '#commits-list' do expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' + expect(page).not_to have_content 'Verified' end - # user changes his email which makes the gpg key unverified + # user changes his email which makes the gpg key verified user.skip_reconfirmation! - user.update_attributes!(email: 'bette.cartwright@example.org') + user.update_attributes!(email: GpgHelpers::User1.emails.first) + + visit namespace_project_commits_path(project.namespace, project, :master) + + 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] + + login_with(user) visit namespace_project_commits_path(project.namespace, project, :master) @@ -243,6 +258,16 @@ describe 'Commits' 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 + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + + visit namespace_project_commits_path(project.namespace, project, :master) + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).to have_content 'Verified' + end end end end -- cgit v1.2.1 From 9816856d055b33de9c47d9e3b73c4acb99c5b5e6 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 22 Jun 2017 14:18:01 +0200 Subject: perform signature update in sidekiq worker --- app/models/gpg_key.rb | 8 +++-- app/models/user.rb | 3 +- app/workers/invalid_gpg_signature_update_worker.rb | 12 ++++++++ config/sidekiq_queues.yml | 1 + spec/features/commits_spec.rb | 14 ++++++--- .../invalid_gpg_signature_update_worker_spec.rb | 36 ++++++++++++++++++++++ 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 app/workers/invalid_gpg_signature_update_worker.rb create mode 100644 spec/workers/invalid_gpg_signature_update_worker_spec.rb diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index ec30658e7ea..a444792581a 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -28,7 +28,7 @@ class GpgKey < ActiveRecord::Base unless: -> { errors.has_key?(:key) } before_validation :extract_fingerprint, :extract_primary_keyid - after_create :update_invalid_gpg_signatures + after_create :update_invalid_gpg_signatures_after_create after_create :notify_user def key=(value) @@ -54,7 +54,7 @@ class GpgKey < ActiveRecord::Base end def update_invalid_gpg_signatures - Gitlab::Gpg::InvalidGpgSignatureUpdater.new(self).run + InvalidGpgSignatureUpdateWorker.perform_async(self.id) end private @@ -74,4 +74,8 @@ class GpgKey < ActiveRecord::Base def notify_user run_after_commit { NotificationService.new.new_gpg_key(self) } end + + def update_invalid_gpg_signatures_after_create + run_after_commit { update_invalid_gpg_signatures } + end end diff --git a/app/models/user.rb b/app/models/user.rb index 791d099605d..931b760df34 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,7 @@ class User < ActiveRecord::Base include IgnorableColumn include FeatureGate include CreatedAtFilterable + include AfterCommitQueue DEFAULT_NOTIFICATION_LEVEL = :participating @@ -515,7 +516,7 @@ class User < ActiveRecord::Base end def update_invalid_gpg_signatures - gpg_keys.each(&:update_invalid_gpg_signatures) + run_after_commit { gpg_keys.each(&:update_invalid_gpg_signatures) } end # Returns the groups a user has access to diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb new file mode 100644 index 00000000000..277dd604aa8 --- /dev/null +++ b/app/workers/invalid_gpg_signature_update_worker.rb @@ -0,0 +1,12 @@ +class InvalidGpgSignatureUpdateWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(gpg_key_id) + if gpg_key = GpgKey.find_by(id: gpg_key_id) + Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run + else + Rails.logger.error("InvalidGpgSignatureUpdateWorker: couldn't find gpg_key with ID=#{gpg_key_id}, skipping job") + end + end +end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 1d9e69a2408..cf0f5719683 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -29,6 +29,7 @@ - [email_receiver, 2] - [emails_on_push, 2] - [mailers, 2] + - [invalid_gpg_signature_update, 2] - [upload_checksum, 1] - [use_key, 1] - [repository_fork, 1] diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 8f89b465160..7635e87e838 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -223,7 +223,9 @@ describe 'Commits' do user = create :user, email: 'unrelated.user@example.org' project.team << [user, :master] - create :gpg_key, key: GpgHelpers::User1.public_key, user: user + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end login_with(user) @@ -235,8 +237,10 @@ describe 'Commits' do end # user changes his email which makes the gpg key verified - user.skip_reconfirmation! - user.update_attributes!(email: GpgHelpers::User1.emails.first) + Sidekiq::Testing.inline! do + user.skip_reconfirmation! + user.update_attributes!(email: GpgHelpers::User1.emails.first) + end visit namespace_project_commits_path(project.namespace, project, :master) @@ -260,7 +264,9 @@ describe 'Commits' do end # user adds the gpg key which makes the signature valid - create :gpg_key, key: GpgHelpers::User1.public_key, user: user + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end visit namespace_project_commits_path(project.namespace, project, :master) diff --git a/spec/workers/invalid_gpg_signature_update_worker_spec.rb b/spec/workers/invalid_gpg_signature_update_worker_spec.rb new file mode 100644 index 00000000000..8d568076e1a --- /dev/null +++ b/spec/workers/invalid_gpg_signature_update_worker_spec.rb @@ -0,0 +1,36 @@ +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 'logs InvalidGpgSignatureUpdateWorker process skipping' do + expect(Rails.logger).to receive(:error) + .with("InvalidGpgSignatureUpdateWorker: couldn't find gpg_key with ID=-1, skipping job") + + described_class.new.perform(nonexisting_gpg_key_id) + end + + 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 -- cgit v1.2.1 From 2ea951454a535ba16693c083c122218b8608329b Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 22 Jun 2017 17:21:03 +0200 Subject: allow removal of gpg key by nullifying signatures --- app/models/gpg_key.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index a444792581a..bd5d833d68c 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -4,6 +4,7 @@ class GpgKey < ActiveRecord::Base KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze belongs_to :user + has_many :gpg_signatures, dependent: :nullify validates :key, presence: true, -- cgit v1.2.1 From 78b5264511a76e481110236e9c14764d9c1b953a Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 23 Jun 2017 22:52:43 +0200 Subject: add gpg commit popover badges --- app/assets/javascripts/commons/bootstrap.js | 1 + app/assets/javascripts/main.js | 6 ++ app/assets/stylesheets/pages/commits.scss | 43 ++++++++++++++ app/helpers/commits_helper.rb | 78 ++++++++++++++++++++++++++ app/views/projects/commit/_signature.html.haml | 4 +- spec/features/commits_spec.rb | 25 +++++++++ 6 files changed, 154 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index 36bfe457be9..510bedbf641 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -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({ diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index e96d51de838..ecf7a677c99 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -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,10 @@ $(function () { return $(el).data('placement') || 'bottom'; } }); + // Initialize popovers + $body.popover({ + selector: '[data-toggle="popover"]' + }); $('.trigger-submit').on('change', function () { return $(this).parents('form').submit(); // Form submitter diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index fd0871ec0b8..54f6156ad8f 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -283,3 +283,46 @@ color: $gl-text-color; } } + +.gpg-badge { + &.valid { + color: $brand-success; + } + + &.invalid { + color: $gray; + } +} + +.gpg-badge-popover-title { + font-weight: normal; +} + +.gpg-badge-popover-icon { + float: left; + font-size: 35px; + line-height: 35px; + width: 32px; + margin-right: $btn-side-margin; + + &.valid { + color: $brand-success; + } + + &.invalid { + color: $gray; + } +} + +.gpg-badge-popover-avatar { + float: left; + margin-bottom: $gl-padding; + + .avatar { + margin-left: 0; + } +} + +.gpg-badge-popover-username { + font-weight: bold; +} diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index d08e346d605..34ba5694288 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -212,4 +212,82 @@ module CommitsHelper [commits, 0] end end + + def commit_gpg_signature_badge(signature) + if signature.valid_signature? + commit_gpg_valid_signature_badge(signature) + else + commit_gpg_invalid_signature_badge(signature) + end + end + + def commit_gpg_valid_signature_badge(signature) + title = capture do + concat content_tag('i', '', class: 'fa fa-check-circle gpg-badge-popover-icon valid', 'aria-hidden' => 'true') + concat 'This commit was signed with a verified signature.' + end + + content = capture do + concat( + content_tag(:div, class: 'gpg-badge-popover-avatar') do + user_avatar(user: signature.gpg_key.user, size: 32) + end + ) + + concat( + content_tag(:div, class: 'gpg-badge-popover-username') do + signature.gpg_key.user.username + end + ) + + concat( + content_tag(:div) do + signature.gpg_key.user.name + end + ) + end + + commit_gpg_signature_badge_with(signature, label: 'Verified', title: title, content: content, css_classes: ['valid']) + end + + def commit_gpg_invalid_signature_badge(signature) + title = capture do + concat content_tag('i', '', class: 'fa fa-question-circle gpg-badge-popover-icon invalid', 'aria-hidden' => 'true') + concat 'This commit was signed with an unverified signature.' + end + commit_gpg_signature_badge_with(signature, label: 'Unverified', title: title, css_classes: ['invalid']) + end + + def commit_gpg_signature_badge_with(signature, label:, title: '', content: '', css_classes: []) + css_classes = %w(btn btn-xs gpg-badge) + css_classes + + content = capture do + concat( + content_tag(:div, class: 'clearfix') do + content + end + ) + + concat "GPG key ID: #{signature.gpg_key_primary_keyid}" + end + + title = capture do + content_tag 'span', class: 'gpg-badge-popover-title' do + title + end + end + + data = { + toggle: 'popover', + html: 'true', + placement: 'auto bottom', + trigger: 'focus', + title: title, + content: content + } + + content_tag :button, class: css_classes, data: data do + label + end + end end diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml index 48665ede6eb..00120a665c5 100644 --- a/app/views/projects/commit/_signature.html.haml +++ b/app/views/projects/commit/_signature.html.haml @@ -1,4 +1,2 @@ - if signature - %a.btn.disabled.btn-xs{ class: ('btn-success' if signature.valid_signature?) } - %i.fa.fa-key{ class: ('fa-inverse' if signature.valid_signature?) } - = signature.valid_signature? ? 'Verified' : 'Unverified' + = commit_gpg_signature_badge(signature) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 7635e87e838..236e3089c6c 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -275,5 +275,30 @@ describe 'Commits' do expect(page).to have_content 'Verified' end end + + it 'shows popover badges', :js do + user = create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard' + project.team << [user, :master] + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + login_with(user) + visit namespace_project_commits_path(project.namespace, project, :master) + + 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 + + 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 + end end end -- cgit v1.2.1 From ee7468e786e434273211586df1408a3c6268e9ed Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 23 Jun 2017 22:53:41 +0200 Subject: we need to update the gpg_key as well --- lib/gitlab/gpg/commit.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 437f13ef311..99d112a51a3 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -32,7 +32,8 @@ module Gitlab def update_signature!(cached_signature) using_keychain do |gpg_key| cached_signature.update_attributes!( - valid_signature: gpg_signature_valid_signature_value(gpg_key) + valid_signature: gpg_signature_valid_signature_value(gpg_key), + gpg_key: gpg_key ) end end -- cgit v1.2.1 From 7fc69a7ed4acb6a583c473799b4169825dcd3777 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Sat, 24 Jun 2017 21:24:49 +0200 Subject: linkify the whole user badge part, not only avatar --- app/assets/stylesheets/pages/commits.scss | 5 +++++ app/helpers/commits_helper.rb | 36 ++++++++++++++++--------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 54f6156ad8f..b1710eee1bf 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -314,6 +314,11 @@ } } +.gpg-badge-popover-user-link { + text-decoration: none; + color: $gl-text-color; +} + .gpg-badge-popover-avatar { float: left; margin-bottom: $gl-padding; diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 34ba5694288..e31baa52e99 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -228,23 +228,25 @@ module CommitsHelper end content = capture do - concat( - content_tag(:div, class: 'gpg-badge-popover-avatar') do - user_avatar(user: signature.gpg_key.user, size: 32) - end - ) - - concat( - content_tag(:div, class: 'gpg-badge-popover-username') do - signature.gpg_key.user.username - end - ) - - concat( - content_tag(:div) do - signature.gpg_key.user.name - end - ) + link_to user_path(signature.gpg_key.user), class: 'gpg-badge-popover-user-link' do + concat( + content_tag(:div, class: 'gpg-badge-popover-avatar') do + user_avatar_without_link(user: signature.gpg_key.user, size: 32) + end + ) + + concat( + content_tag(:div, class: 'gpg-badge-popover-username') do + signature.gpg_key.user.username + end + ) + + concat( + content_tag(:div) do + signature.gpg_key.user.name + end + ) + end end commit_gpg_signature_badge_with(signature, label: 'Verified', title: title, content: content, css_classes: ['valid']) -- cgit v1.2.1 From afd7582af6a20d72b1d941d9849f331aee8f994a Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 26 Jun 2017 09:13:36 +0200 Subject: extract variable --- spec/lib/gitlab/gpg/commit_spec.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 387ce8f74b4..661956b7bb7 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -3,6 +3,7 @@ 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 @@ -19,7 +20,7 @@ RSpec.describe Gitlab::Gpg::Commit do raw_commit = double(:raw_commit, signature: [ GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data - ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + ], sha: commit_sha) allow(raw_commit).to receive :save! create :commit, git_commit: raw_commit, project: project @@ -27,7 +28,7 @@ RSpec.describe Gitlab::Gpg::Commit do it 'returns a valid signature' do expect(described_class.new(commit).signature).to have_attributes( - commit_sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', + commit_sha: commit_sha, project: project, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, @@ -54,7 +55,7 @@ RSpec.describe Gitlab::Gpg::Commit do raw_commit = double(:raw_commit, signature: [ GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data - ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + ], sha: commit_sha) allow(raw_commit).to receive :save! create :commit, git_commit: raw_commit, project: project @@ -62,7 +63,7 @@ RSpec.describe Gitlab::Gpg::Commit do it 'returns an invalid signature' do expect(described_class.new(commit).signature).to have_attributes( - commit_sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', + commit_sha: commit_sha, project: project, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, @@ -87,7 +88,7 @@ RSpec.describe Gitlab::Gpg::Commit do raw_commit = double(:raw_commit, signature: [ GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data - ], sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33') + ], sha: commit_sha) allow(raw_commit).to receive :save! create :commit, @@ -97,7 +98,7 @@ RSpec.describe Gitlab::Gpg::Commit do it 'returns an invalid signature' do expect(described_class.new(commit).signature).to have_attributes( - commit_sha: '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', + commit_sha: commit_sha, project: project, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, -- cgit v1.2.1 From 5013f3a8167bb7c665b19f44ae66e543fe0b2fce Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 26 Jun 2017 14:29:01 +0200 Subject: use updated gitlab-test repo for signed commits --- spec/features/commits_spec.rb | 24 +++++------------------- spec/support/test_env.rb | 1 + 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 236e3089c6c..274247fc4d3 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'fileutils' describe 'Commits' do include CiStatusHelper @@ -206,19 +205,6 @@ describe 'Commits' do end describe 'GPG signed commits' do - before do - # FIXME: add this to the test repository directly - remote_path = project.repository.path_to_repo - Dir.mktmpdir do |dir| - FileUtils.cd dir do - `git clone --quiet #{remote_path} .` - `git commit --quiet -S#{GpgHelpers::User1.primary_keyid} --allow-empty -m "signed commit by nannie bernhard"` - `git commit --quiet -S#{GpgHelpers::User2.primary_keyid} --allow-empty -m "signed commit by bette cartwright"` - `git push --quiet` - end - end - end - 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] @@ -229,7 +215,7 @@ describe 'Commits' do login_with(user) - visit namespace_project_commits_path(project.namespace, project, :master) + visit namespace_project_commits_path(project.namespace, project, :'signed-commits') within '#commits-list' do expect(page).to have_content 'Unverified' @@ -242,7 +228,7 @@ describe 'Commits' do user.update_attributes!(email: GpgHelpers::User1.emails.first) end - visit namespace_project_commits_path(project.namespace, project, :master) + visit namespace_project_commits_path(project.namespace, project, :'signed-commits') within '#commits-list' do expect(page).to have_content 'Unverified' @@ -256,7 +242,7 @@ describe 'Commits' do login_with(user) - visit namespace_project_commits_path(project.namespace, project, :master) + visit namespace_project_commits_path(project.namespace, project, :'signed-commits') within '#commits-list' do expect(page).to have_content 'Unverified' @@ -268,7 +254,7 @@ describe 'Commits' do create :gpg_key, key: GpgHelpers::User1.public_key, user: user end - visit namespace_project_commits_path(project.namespace, project, :master) + visit namespace_project_commits_path(project.namespace, project, :'signed-commits') within '#commits-list' do expect(page).to have_content 'Unverified' @@ -284,7 +270,7 @@ describe 'Commits' do end login_with(user) - visit namespace_project_commits_path(project.namespace, project, :master) + visit namespace_project_commits_path(project.namespace, project, :'signed-commits') click_on 'Verified' within '.popover' do diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index c32c05b03e2..7682bdf8cd0 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -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', -- cgit v1.2.1 From 4648d0016f67c9b780e3fff475e458fc28825daf Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 26 Jun 2017 14:41:17 +0200 Subject: popover trigger needs to be defined in js init According to https://github.com/twbs/bootstrap/issues/10547 it's not possible to have the trigger defined on the delegated element, i.e. not defined as a data attribute. --- app/assets/javascripts/main.js | 3 ++- app/helpers/commits_helper.rb | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index ecf7a677c99..d039ca9e47c 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -251,7 +251,8 @@ $(function () { }); // Initialize popovers $body.popover({ - selector: '[data-toggle="popover"]' + selector: '[data-toggle="popover"]', + trigger: 'focus' }); $('.trigger-submit').on('change', function () { return $(this).parents('form').submit(); diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index e31baa52e99..42e9379d1f4 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -283,7 +283,6 @@ module CommitsHelper toggle: 'popover', html: 'true', placement: 'auto bottom', - trigger: 'focus', title: title, content: content } -- cgit v1.2.1 From 9a759c620179c72821fa6217c138036b57ea3695 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 28 Jun 2017 12:51:18 +0200 Subject: add changelog --- changelogs/unreleased/feature-gpg-signed-commits.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/feature-gpg-signed-commits.yml diff --git a/changelogs/unreleased/feature-gpg-signed-commits.yml b/changelogs/unreleased/feature-gpg-signed-commits.yml new file mode 100644 index 00000000000..99bc5a309ef --- /dev/null +++ b/changelogs/unreleased/feature-gpg-signed-commits.yml @@ -0,0 +1,4 @@ +--- +title: GPG signed commits integration +merge_request: 9546 +author: Alexis Reigel -- cgit v1.2.1 From e9515dff845dfbbb51e556e4e6a4f9cf951cf239 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 28 Jun 2017 13:08:29 +0200 Subject: remove the :gpg rspec tag since everything (except the CurrentKeyChain method) operates on a tempoary keychain anyway we don't need this anymore. --- spec/features/profiles/gpg_keys_spec.rb | 2 +- spec/lib/gitlab/gpg_spec.rb | 10 ++++++++-- spec/models/gpg_key_spec.rb | 6 +++--- spec/spec_helper.rb | 6 ------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index 552cca4a84e..350126523b0 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Profile > GPG Keys', :gpg do +feature 'Profile > GPG Keys' do let(:user) { create(:user, email: GpgHelpers::User2.emails.first) } before do diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index edf7405d7f1..497fbeab5d5 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -44,8 +44,14 @@ describe Gitlab::Gpg do end end -describe Gitlab::Gpg::CurrentKeyChain, :gpg do - describe '.add', :gpg do +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 [] diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 3cb1723cc12..312e026a78e 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -13,7 +13,7 @@ describe GpgKey do it { is_expected.not_to allow_value('BEGIN PGP').for(:key) } end - context 'callbacks', :gpg do + 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) @@ -45,7 +45,7 @@ describe GpgKey do end end - describe '#emails', :gpg do + describe '#emails' do it 'returns the emails from the gpg key' do gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key @@ -53,7 +53,7 @@ describe GpgKey do end end - describe '#emails_with_verified_status', :gpg do + 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a0df233507b..e7329210896 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -141,12 +141,6 @@ RSpec.configure do |config| config.around(:each, :postgresql) do |example| example.run if Gitlab::Database.postgresql? end - - config.around(:each, :gpg) do |example| - Gitlab::Gpg.using_tmp_keychain do - example.run - end - end end FactoryGirl::SyntaxRunner.class_eval do -- cgit v1.2.1 From bd476c1b4cd3399e684cc833a350b1f34c20b115 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 28 Jun 2017 13:24:52 +0200 Subject: use sign_in instead of login_with --- spec/features/commits_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 274247fc4d3..709df6336fe 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -213,7 +213,7 @@ describe 'Commits' do create :gpg_key, key: GpgHelpers::User1.public_key, user: user end - login_with(user) + sign_in(user) visit namespace_project_commits_path(project.namespace, project, :'signed-commits') @@ -240,7 +240,7 @@ describe 'Commits' do user = create :user, email: GpgHelpers::User1.emails.first project.team << [user, :master] - login_with(user) + sign_in(user) visit namespace_project_commits_path(project.namespace, project, :'signed-commits') @@ -269,7 +269,7 @@ describe 'Commits' do create :gpg_key, key: GpgHelpers::User1.public_key, user: user end - login_with(user) + sign_in(user) visit namespace_project_commits_path(project.namespace, project, :'signed-commits') click_on 'Verified' -- cgit v1.2.1 From 28c75fc1a87f8190c89666f8b6e3436311d024ce Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 29 Jun 2017 14:31:55 +0200 Subject: documentation for gpg signed commits --- doc/README.md | 1 + .../img/profile_settings_gpg_keys.png | Bin 0 -> 32699 bytes .../img/profile_settings_gpg_keys_paste_pub.png | Bin 0 -> 24514 bytes .../img/profile_settings_gpg_keys_single_key.png | Bin 0 -> 10331 bytes .../img/project_signed_and_unsigned_commits.png | Bin 0 -> 112812 bytes .../project_signed_commit_unverified_signature.png | Bin 0 -> 9542 bytes .../project_signed_commit_verified_signature.png | Bin 0 -> 14029 bytes doc/workflow/gpg_signed_commits/index.md | 55 +++++++++++++++++++++ 8 files changed, 56 insertions(+) create mode 100644 doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys.png create mode 100644 doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png create mode 100644 doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png create mode 100644 doc/workflow/gpg_signed_commits/img/project_signed_and_unsigned_commits.png create mode 100644 doc/workflow/gpg_signed_commits/img/project_signed_commit_unverified_signature.png create mode 100644 doc/workflow/gpg_signed_commits/img/project_signed_commit_verified_signature.png create mode 100644 doc/workflow/gpg_signed_commits/index.md diff --git a/doc/README.md b/doc/README.md index ac7311a8c13..5537f54ab2b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -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 diff --git a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys.png b/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys.png new file mode 100644 index 00000000000..e525083918b Binary files /dev/null and b/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys.png differ diff --git a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png b/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png new file mode 100644 index 00000000000..8e26d98f1b0 Binary files /dev/null and b/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png differ diff --git a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png b/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png new file mode 100644 index 00000000000..f715c46adc3 Binary files /dev/null and b/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png differ diff --git a/doc/workflow/gpg_signed_commits/img/project_signed_and_unsigned_commits.png b/doc/workflow/gpg_signed_commits/img/project_signed_and_unsigned_commits.png new file mode 100644 index 00000000000..16ec2d031ae Binary files /dev/null and b/doc/workflow/gpg_signed_commits/img/project_signed_and_unsigned_commits.png differ diff --git a/doc/workflow/gpg_signed_commits/img/project_signed_commit_unverified_signature.png b/doc/workflow/gpg_signed_commits/img/project_signed_commit_unverified_signature.png new file mode 100644 index 00000000000..22565cf7c7e Binary files /dev/null and b/doc/workflow/gpg_signed_commits/img/project_signed_commit_unverified_signature.png differ diff --git a/doc/workflow/gpg_signed_commits/img/project_signed_commit_verified_signature.png b/doc/workflow/gpg_signed_commits/img/project_signed_commit_verified_signature.png new file mode 100644 index 00000000000..1778b2ddf2b Binary files /dev/null and b/doc/workflow/gpg_signed_commits/img/project_signed_commit_verified_signature.png differ diff --git a/doc/workflow/gpg_signed_commits/index.md b/doc/workflow/gpg_signed_commits/index.md new file mode 100644 index 00000000000..041c681ba63 --- /dev/null +++ b/doc/workflow/gpg_signed_commits/index.md @@ -0,0 +1,55 @@ +# Signing commits with GPG + +## Getting started + +- [Git Tools - Signing Your Work](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) + +## 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. + +## 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) -- cgit v1.2.1 From 3b7ac360cff04a5c1be83ee13b1354f07021c80d Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 30 Jun 2017 12:48:08 +0200 Subject: position gpg badge first on commit line --- app/views/projects/commit/_commit_box.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 1a0c70ef803..419fbe99af8 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,12 +1,12 @@ .page-content-header .header-main-content + = render partial: 'signature', object: @commit.signature %strong #{ s_('CommitBoxTitle|Commit') } %span.commit-sha= @commit.short_id = clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard")) %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} - = render partial: 'signature', object: @commit.signature %span= s_('ByAuthor|by') = author_avatar(@commit, size: 24) %strong -- cgit v1.2.1 From ade54803a7f8b1470320fc6fa5871f2ec208eb0f Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 3 Jul 2017 09:41:32 +0200 Subject: add help links to gpg commits / gpg settings --- app/assets/stylesheets/pages/commits.scss | 5 +++++ app/helpers/commits_helper.rb | 7 +++++++ app/views/profiles/gpg_keys/index.html.haml | 5 ++++- doc/workflow/gpg_signed_commits/index.md | 2 ++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index b1710eee1bf..5de98cfc7af 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -331,3 +331,8 @@ .gpg-badge-popover-username { font-weight: bold; } + +.commit .gpg-badge-popover-help-link { + display: block; + color: $link-color; +} diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 42e9379d1f4..60acc1e2f82 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -271,6 +271,13 @@ module CommitsHelper ) concat "GPG key ID: #{signature.gpg_key_primary_keyid}" + concat( + link_to( + 'Learn about signing commits', + help_page_path('workflow/gpg_signed_commits/index.md'), + class: 'gpg-badge-popover-help-link' + ) + ) end title = capture do diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml index 30066522766..8331daeeb75 100644 --- a/app/views/profiles/gpg_keys/index.html.haml +++ b/app/views/profiles/gpg_keys/index.html.haml @@ -9,7 +9,10 @@ GPG keys allow you to verify signed commits. .col-lg-9 %h5.prepend-top-0 - Add an GPG key + 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 diff --git a/doc/workflow/gpg_signed_commits/index.md b/doc/workflow/gpg_signed_commits/index.md index 041c681ba63..f7f5492c35a 100644 --- a/doc/workflow/gpg_signed_commits/index.md +++ b/doc/workflow/gpg_signed_commits/index.md @@ -3,6 +3,8 @@ ## 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 -- cgit v1.2.1 From a06494bf71b589d2b9f5c80710b6c8e0749fc210 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 4 Jul 2017 13:46:33 +0200 Subject: no need for html_safe --- app/views/profiles/gpg_keys/_key.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index d7450a22f4c..b4b9aa07190 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -2,7 +2,9 @@ .pull-left.append-right-10 = icon 'key', class: "settings-list-icon hidden-xs" .key-list-item-info - = key.emails_with_verified_status.map { |email, verified| verified_email_badge(email, verified) }.join(' ').html_safe + - key.emails_with_verified_status.map do |email, verified| + = verified_email_badge(email, verified) + .description = key.fingerprint .pull-right -- cgit v1.2.1 From 075dae65b1a5b57788e4823b09bdcb8fa6eeaf8a Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 5 Jul 2017 13:11:18 +0200 Subject: find_by_id -> find_by(:id, ...) --- app/mailers/emails/profile.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 4580e1c83bd..c401030e34a 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -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 @@ -24,7 +24,7 @@ module Emails end def new_gpg_key_email(gpg_key_id) - @gpg_key = GpgKey.find_by_id(gpg_key_id) + @gpg_key = GpgKey.find_by(id: gpg_key_id) return unless @gpg_key -- cgit v1.2.1 From d9fd3709abb7897785ac111c217b532663313abd Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 5 Jul 2017 13:16:50 +0200 Subject: use hash instead of 2d array --- app/models/gpg_key.rb | 2 +- spec/models/gpg_key_spec.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index bd5d833d68c..8cfccef0854 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -47,7 +47,7 @@ class GpgKey < ActiveRecord::Base email, email == user.email ] - end + end.to_h end def verified? diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 312e026a78e..88b5eb79b59 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -58,10 +58,10 @@ describe GpgKey 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 match_array [ - ['bette.cartwright@example.com', true], - ['bette.cartwright@example.net', false] - ] + expect(gpg_key.emails_with_verified_status).to eq( + 'bette.cartwright@example.com' => true, + 'bette.cartwright@example.net' => false + ) end end -- cgit v1.2.1 From e79e2ae1f4b671488b31428f7a6506a245a7bddc Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 5 Jul 2017 14:03:36 +0200 Subject: validate presence of user on gpg_key --- app/models/gpg_key.rb | 2 ++ spec/models/gpg_key_spec.rb | 1 + 2 files changed, 3 insertions(+) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 8cfccef0854..612d954b1c5 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -6,6 +6,8 @@ class GpgKey < ActiveRecord::Base belongs_to :user has_many :gpg_signatures, dependent: :nullify + validates :user, presence: true + validates :key, presence: true, uniqueness: true, diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 88b5eb79b59..ffbf8760e86 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -6,6 +6,7 @@ describe GpgKey do 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").for(:key) } -- cgit v1.2.1 From 084cc718f759a37c8fc5535930daeee5e819c30f Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 5 Jul 2017 14:23:23 +0200 Subject: use after_commit instead of AfterCommitQueue --- app/models/gpg_key.rb | 12 +++--------- app/models/user.rb | 5 ++--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 612d954b1c5..050245bd502 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -1,6 +1,4 @@ class GpgKey < ActiveRecord::Base - include AfterCommitQueue - KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze belongs_to :user @@ -31,8 +29,8 @@ class GpgKey < ActiveRecord::Base unless: -> { errors.has_key?(:key) } before_validation :extract_fingerprint, :extract_primary_keyid - after_create :update_invalid_gpg_signatures_after_create - after_create :notify_user + after_commit :update_invalid_gpg_signatures, on: :create + after_commit :notify_user, on: :create def key=(value) value.strip! unless value.blank? @@ -75,10 +73,6 @@ class GpgKey < ActiveRecord::Base end def notify_user - run_after_commit { NotificationService.new.new_gpg_key(self) } - end - - def update_invalid_gpg_signatures_after_create - run_after_commit { update_invalid_gpg_signatures } + NotificationService.new.new_gpg_key(self) end end diff --git a/app/models/user.rb b/app/models/user.rb index 931b760df34..03a76f4fa23 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,7 +13,6 @@ class User < ActiveRecord::Base include IgnorableColumn include FeatureGate include CreatedAtFilterable - include AfterCommitQueue DEFAULT_NOTIFICATION_LEVEL = :participating @@ -156,10 +155,10 @@ class User < ActiveRecord::Base before_validation :set_public_email, if: :public_email_changed? after_update :update_emails_with_primary_email, if: :email_changed? - after_update :update_invalid_gpg_signatures, if: :email_changed? 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 @@ -516,7 +515,7 @@ class User < ActiveRecord::Base end def update_invalid_gpg_signatures - run_after_commit { gpg_keys.each(&:update_invalid_gpg_signatures) } + gpg_keys.each(&:update_invalid_gpg_signatures) end # Returns the groups a user has access to -- cgit v1.2.1 From 36c05b311c830aef25ecb7ad4416ac77a5c98651 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 5 Jul 2017 20:59:17 +0200 Subject: convert gpg badge helper methods to partials --- app/assets/stylesheets/pages/commits.scss | 1 + app/helpers/commits_helper.rb | 86 ---------------------- .../commit/_invalid_signature_badge.html.haml | 7 ++ app/views/projects/commit/_signature.html.haml | 5 +- .../projects/commit/_signature_badge.html.haml | 17 +++++ .../commit/_valid_signature_badge.html.haml | 19 +++++ 6 files changed, 48 insertions(+), 87 deletions(-) create mode 100644 app/views/projects/commit/_invalid_signature_badge.html.haml create mode 100644 app/views/projects/commit/_signature_badge.html.haml create mode 100644 app/views/projects/commit/_valid_signature_badge.html.haml diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 5de98cfc7af..b6e9053fbce 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -295,6 +295,7 @@ } .gpg-badge-popover-title { + display: inline; font-weight: normal; } diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 60acc1e2f82..d08e346d605 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -212,90 +212,4 @@ module CommitsHelper [commits, 0] end end - - def commit_gpg_signature_badge(signature) - if signature.valid_signature? - commit_gpg_valid_signature_badge(signature) - else - commit_gpg_invalid_signature_badge(signature) - end - end - - def commit_gpg_valid_signature_badge(signature) - title = capture do - concat content_tag('i', '', class: 'fa fa-check-circle gpg-badge-popover-icon valid', 'aria-hidden' => 'true') - concat 'This commit was signed with a verified signature.' - end - - content = capture do - link_to user_path(signature.gpg_key.user), class: 'gpg-badge-popover-user-link' do - concat( - content_tag(:div, class: 'gpg-badge-popover-avatar') do - user_avatar_without_link(user: signature.gpg_key.user, size: 32) - end - ) - - concat( - content_tag(:div, class: 'gpg-badge-popover-username') do - signature.gpg_key.user.username - end - ) - - concat( - content_tag(:div) do - signature.gpg_key.user.name - end - ) - end - end - - commit_gpg_signature_badge_with(signature, label: 'Verified', title: title, content: content, css_classes: ['valid']) - end - - def commit_gpg_invalid_signature_badge(signature) - title = capture do - concat content_tag('i', '', class: 'fa fa-question-circle gpg-badge-popover-icon invalid', 'aria-hidden' => 'true') - concat 'This commit was signed with an unverified signature.' - end - commit_gpg_signature_badge_with(signature, label: 'Unverified', title: title, css_classes: ['invalid']) - end - - def commit_gpg_signature_badge_with(signature, label:, title: '', content: '', css_classes: []) - css_classes = %w(btn btn-xs gpg-badge) + css_classes - - content = capture do - concat( - content_tag(:div, class: 'clearfix') do - content - end - ) - - concat "GPG key ID: #{signature.gpg_key_primary_keyid}" - concat( - link_to( - 'Learn about signing commits', - help_page_path('workflow/gpg_signed_commits/index.md'), - class: 'gpg-badge-popover-help-link' - ) - ) - end - - title = capture do - content_tag 'span', class: 'gpg-badge-popover-title' do - title - end - end - - data = { - toggle: 'popover', - html: 'true', - placement: 'auto bottom', - title: title, - content: content - } - - content_tag :button, class: css_classes, data: data do - label - end - end end diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml new file mode 100644 index 00000000000..29c787bd324 --- /dev/null +++ b/app/views/projects/commit/_invalid_signature_badge.html.haml @@ -0,0 +1,7 @@ +- title = capture do + %i{ class: 'fa fa-question-circle gpg-badge-popover-icon invalid', 'aria-hidden' => 'true' } + This commit was signed with an unverified signature. + +- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml index 00120a665c5..60fa52557ef 100644 --- a/app/views/projects/commit/_signature.html.haml +++ b/app/views/projects/commit/_signature.html.haml @@ -1,2 +1,5 @@ - if signature - = commit_gpg_signature_badge(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 } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml new file mode 100644 index 00000000000..2e046c1f684 --- /dev/null +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -0,0 +1,17 @@ +- css_classes = %w(btn btn-xs gpg-badge) + css_classes + +- title = capture do + .gpg-badge-popover-title + = title + +- content = capture do + .clearfix + = content + + GPG key ID: + = signature.gpg_key_primary_keyid + + = link_to('Learn about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-badge-popover-help-link') + +%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto bottom', title: title, content: content } } + = label diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml new file mode 100644 index 00000000000..47226466b85 --- /dev/null +++ b/app/views/projects/commit/_valid_signature_badge.html.haml @@ -0,0 +1,19 @@ +- title = capture do + %i{ class: 'fa fa-check-circle gpg-badge-popover-icon valid', 'aria-hidden' => 'true' } + This commit was signed with a verified signature. + +- content = capture do + - gpg_key = signature.gpg_key + + = link_to user_path(gpg_key.user), class: 'gpg-badge-popover-user-link' do + .gpg-badge-popover-avatar + = user_avatar_without_link(user: signature.gpg_key.user, size: 32) + + .gpg-badge-popover-username + = gpg_key.user.username + + %div= gpg_key.user.name + +- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] } + += render partial: 'projects/commit/signature_badge', locals: locals -- cgit v1.2.1 From 4f7ba8f2861b39d3a7697eb99e3fbaaf39f32643 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 6 Jul 2017 08:03:16 +0200 Subject: fix memoization --- lib/gitlab/gpg/commit.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 99d112a51a3..718e77ecadc 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -22,7 +22,6 @@ module Gitlab using_keychain do |gpg_key| if gpg_key Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) - @verified_signature = nil end create_cached_signature!(gpg_key) @@ -50,6 +49,7 @@ module Gitlab if gpg_key Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) + @verified_signature = nil end yield gpg_key @@ -58,7 +58,7 @@ module Gitlab def verified_signature @verified_signature ||= GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature| - return verified_signature + break verified_signature end end -- cgit v1.2.1 From a7d2ebe508b6dde3b3ae37c5a54fc78719b199b3 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 6 Jul 2017 10:17:09 +0200 Subject: simplify fetching of commit --- app/models/gpg_signature.rb | 4 ++++ lib/gitlab/gpg/invalid_gpg_signature_updater.rb | 4 +--- spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb | 2 +- spec/models/gpg_signature_spec.rb | 13 +++++++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 4fe9c3210ff..0ef335bf8a9 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -5,4 +5,8 @@ class GpgSignature < ActiveRecord::Base validates :commit_sha, presence: true validates :project, presence: true validates :gpg_key_primary_keyid, presence: true + + def commit + project.commit(commit_sha) + end end diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index 06e4823de32..860e1e1035c 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -10,9 +10,7 @@ module Gitlab .where(valid_signature: false) .where(gpg_key_primary_keyid: @gpg_key.primary_keyid) .find_each do |gpg_signature| - raw_commit = Gitlab::Git::Commit.find(gpg_signature.project.repository, gpg_signature.commit_sha) - commit = ::Commit.new(raw_commit, gpg_signature.project) - Gitlab::Gpg::Commit.new(commit).update_signature!(gpg_signature) + Gitlab::Gpg::Commit.new(gpg_signature.commit).update_signature!(gpg_signature) end end end diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index 8b60b36452b..c16f15bf4bf 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do end before do - allow(Gitlab::Git::Commit).to receive(:find).with(kind_of(Repository), commit_sha).and_return(raw_commit) + allow_any_instance_of(GpgSignature).to receive(:commit).and_return(commit) end context 'gpg signature did not have an associated gpg key' do diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb index b3f84262874..b6f256e61ee 100644 --- a/spec/models/gpg_signature_spec.rb +++ b/spec/models/gpg_signature_spec.rb @@ -12,4 +12,17 @@ RSpec.describe GpgSignature do it { is_expected.to validate_presence_of(:project) } 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 -- cgit v1.2.1 From 7f03282f0ff45948d3d27efe007ba77e24e19fa5 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 6 Jul 2017 10:26:31 +0200 Subject: remove duplicate statement --- lib/gitlab/gpg/commit.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 718e77ecadc..50e8d71bb13 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -20,10 +20,6 @@ module Gitlab return cached_signature if cached_signature.present? using_keychain do |gpg_key| - if gpg_key - Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) - end - create_cached_signature!(gpg_key) end end -- cgit v1.2.1 From b66e3726dc377c2bb5c92983db4ec4c8d27237c4 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 6 Jul 2017 11:15:31 +0200 Subject: also update gpg_signatures when gpg_key is null --- lib/gitlab/gpg/invalid_gpg_signature_updater.rb | 2 +- .../gpg/invalid_gpg_signature_updater_spec.rb | 70 +++++++++++++++++----- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index 860e1e1035c..1782e20dcab 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -7,7 +7,7 @@ module Gitlab def run GpgSignature - .where(valid_signature: false) + .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) diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index c16f15bf4bf..5a81a86b93c 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -19,29 +19,60 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do create :commit, git_commit: raw_commit, project: project end - let!(: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 - before do allow_any_instance_of(GpgSignature).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.gpg_key).to eq gpg_key + 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.gpg_key).to be_nil + 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 create :gpg_key, key: GpgHelpers::User1.public_key, user: user - expect(gpg_signature.reload.valid_signature).to be_truthy + expect(invalid_gpg_signature.reload.valid_signature).to be_truthy end it 'keeps the signature at being invalid when an unrelated gpg key is added' do @@ -50,7 +81,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do key: GpgHelpers::User2.public_key, user: user - expect(gpg_signature.reload.valid_signature).to be_falsey + expect(invalid_gpg_signature.reload.valid_signature).to be_falsey end end @@ -61,17 +92,26 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do 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 create :gpg_key, key: GpgHelpers::User1.public_key, user: user - expect(gpg_signature.reload.valid_signature).to be_falsey + 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(gpg_signature.reload.valid_signature).to be_truthy + expect(invalid_gpg_signature.reload.valid_signature).to be_truthy end it 'keeps the signature at being invalid when the changed email address is still unrelated' do @@ -79,12 +119,12 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do key: GpgHelpers::User1.public_key, user: user - expect(gpg_signature.reload.valid_signature).to be_falsey + expect(invalid_gpg_signature.reload.valid_signature).to be_falsey # InvalidGpgSignatureUpdater is called by the after_update hook user.update_attributes!(email: 'still.unrelated@example.com') - expect(gpg_signature.reload.valid_signature).to be_falsey + expect(invalid_gpg_signature.reload.valid_signature).to be_falsey end end end -- cgit v1.2.1 From deb474b4137c8ab4ce16f4d46e011be593f0de60 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 6 Jul 2017 11:55:56 +0200 Subject: extract common method --- lib/gitlab/gpg.rb | 22 +++++++++------------- spec/lib/gitlab/gpg_spec.rb | 29 +++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 258901bb238..582347019e5 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -8,10 +8,8 @@ module Gitlab def add(key) GPGME::Key.import(key) end - end - def fingerprints_from_key(key) - using_tmp_keychain do + def fingerprints_from_key(key) import = GPGME::Key.import(key) return [] if import.imported == 0 @@ -20,13 +18,15 @@ module Gitlab end end - def primary_keyids_from_key(key) + def fingerprints_from_key(key) using_tmp_keychain do - import = GPGME::Key.import(key) - - return [] if import.imported == 0 + CurrentKeyChain.fingerprints_from_key(key) + end + end - fingerprints = import.imports.map(&:fingerprint) + 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 @@ -34,11 +34,7 @@ module Gitlab def emails_from_key(key) using_tmp_keychain do - import = GPGME::Key.import(key) - - return [] if import.imported == 0 - - fingerprints = import.imports.map(&:fingerprint) + fingerprints = CurrentKeyChain.fingerprints_from_key(key) GPGME::Key.find(:public, fingerprints).flat_map { |raw_key| raw_key.uids.map(&:email) } end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 497fbeab5d5..ebb7720eaea 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -2,16 +2,15 @@ require 'rails_helper' describe Gitlab::Gpg do 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] + 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 an empty array when the key is invalid' do - expect( - described_class.fingerprints_from_key('bogus') - ).to eq [] + 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 @@ -65,4 +64,18 @@ describe Gitlab::Gpg::CurrentKeyChain do ) 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 -- cgit v1.2.1 From 3729c3a7c7cdfeed7a0fc363d18a67e9956bdf07 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 6 Jul 2017 16:30:43 +0200 Subject: use the correct flex classes on the commits list --- app/views/projects/commits/_commit.html.haml | 4 ++-- app/views/projects/commits/_commits.html.haml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 5f67727514a..b7f18d44838 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -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,7 +36,7 @@ #{ commit_text.html_safe } - .commit-actions.flex-row.hidden-xs + .commit-actions.hidden-xs - if commit.status(ref) = render_commit_status(commit, ref: ref) = render partial: 'projects/commit/signature', object: commit.signature diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index c764e35dd2a..d14897428d0 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -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 -- cgit v1.2.1 From 8ccce9d54541de5cbc8e5ce4a33fcefd402bdda4 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 6 Jul 2017 16:36:57 +0200 Subject: use existing status-box css class for gpg badge --- app/assets/stylesheets/pages/commits.scss | 16 +++++++++------- .../projects/commit/_invalid_signature_badge.html.haml | 2 +- app/views/projects/commit/_signature_badge.html.haml | 6 +++--- .../projects/commit/_valid_signature_badge.html.haml | 8 ++++---- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index b6e9053fbce..16a8c399bc6 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -284,22 +284,24 @@ } } -.gpg-badge { +.gpg-status-box { &.valid { color: $brand-success; + border: 1px solid $brand-success; } &.invalid { color: $gray; + border: 1px solid $gray; } } -.gpg-badge-popover-title { +.gpg-popover-title { display: inline; font-weight: normal; } -.gpg-badge-popover-icon { +.gpg-popover-icon { float: left; font-size: 35px; line-height: 35px; @@ -315,12 +317,12 @@ } } -.gpg-badge-popover-user-link { +.gpg-popover-user-link { text-decoration: none; color: $gl-text-color; } -.gpg-badge-popover-avatar { +.gpg-popover-avatar { float: left; margin-bottom: $gl-padding; @@ -329,11 +331,11 @@ } } -.gpg-badge-popover-username { +.gpg-popover-username { font-weight: bold; } -.commit .gpg-badge-popover-help-link { +.commit .gpg-popover-help-link { display: block; color: $link-color; } diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml index 29c787bd324..24b6040dcbb 100644 --- a/app/views/projects/commit/_invalid_signature_badge.html.haml +++ b/app/views/projects/commit/_invalid_signature_badge.html.haml @@ -1,5 +1,5 @@ - title = capture do - %i{ class: 'fa fa-question-circle gpg-badge-popover-icon invalid', 'aria-hidden' => 'true' } + %i{ class: 'fa fa-question-circle gpg-popover-icon invalid', 'aria-hidden' => 'true' } This commit was signed with an unverified signature. - locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index 2e046c1f684..88eb0505424 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -1,7 +1,7 @@ -- css_classes = %w(btn btn-xs gpg-badge) + css_classes +- css_classes = %w(btn status-box gpg-status-box) + css_classes - title = capture do - .gpg-badge-popover-title + .gpg-popover-title = title - content = capture do @@ -11,7 +11,7 @@ GPG key ID: = signature.gpg_key_primary_keyid - = link_to('Learn about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-badge-popover-help-link') + = link_to('Learn 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 bottom', title: title, content: content } } = label diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml index 47226466b85..a94fe9ef9a1 100644 --- a/app/views/projects/commit/_valid_signature_badge.html.haml +++ b/app/views/projects/commit/_valid_signature_badge.html.haml @@ -1,15 +1,15 @@ - title = capture do - %i{ class: 'fa fa-check-circle gpg-badge-popover-icon valid', 'aria-hidden' => 'true' } + %i{ class: 'fa fa-check-circle gpg-popover-icon valid', 'aria-hidden' => 'true' } This commit was signed with a verified signature. - content = capture do - gpg_key = signature.gpg_key - = link_to user_path(gpg_key.user), class: 'gpg-badge-popover-user-link' do - .gpg-badge-popover-avatar + = link_to user_path(gpg_key.user), class: 'gpg-popover-user-link' do + .gpg-popover-avatar = user_avatar_without_link(user: signature.gpg_key.user, size: 32) - .gpg-badge-popover-username + .gpg-popover-username = gpg_key.user.username %div= gpg_key.user.name -- cgit v1.2.1 From 4c5d4a69f0b5a813d2cb53e6f9af925cd3f9e8cb Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 10 Jul 2017 10:02:41 +0200 Subject: improve spacing / alignments in gpg popup --- app/assets/stylesheets/pages/commits.scss | 26 +++++++--------------- .../commit/_invalid_signature_badge.html.haml | 3 ++- .../projects/commit/_signature_badge.html.haml | 8 +++---- .../commit/_valid_signature_badge.html.haml | 12 +++++----- spec/features/commits_spec.rb | 14 ++++++------ 5 files changed, 27 insertions(+), 36 deletions(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 16a8c399bc6..62a1296f77e 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -296,16 +296,16 @@ } } -.gpg-popover-title { - display: inline; +.gpg-popover-status { + display: flex; + align-items: center; font-weight: normal; + line-height: 1.5; } .gpg-popover-icon { - float: left; font-size: 35px; - line-height: 35px; - width: 32px; + // same margin as .s32.avatar margin-right: $btn-side-margin; &.valid { @@ -318,23 +318,13 @@ } .gpg-popover-user-link { + display: flex; + align-items: center; + margin-bottom: $gl-padding / 2; text-decoration: none; color: $gl-text-color; } -.gpg-popover-avatar { - float: left; - margin-bottom: $gl-padding; - - .avatar { - margin-left: 0; - } -} - -.gpg-popover-username { - font-weight: bold; -} - .commit .gpg-popover-help-link { display: block; color: $link-color; diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml index 24b6040dcbb..0e94d0b2d59 100644 --- a/app/views/projects/commit/_invalid_signature_badge.html.haml +++ b/app/views/projects/commit/_invalid_signature_badge.html.haml @@ -1,6 +1,7 @@ - title = capture do %i{ class: 'fa fa-question-circle gpg-popover-icon invalid', 'aria-hidden' => 'true' } - This commit was signed with an unverified signature. + %div + This commit was signed with an unverified signature. - locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index 88eb0505424..8e09a9333aa 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -1,17 +1,17 @@ - css_classes = %w(btn status-box gpg-status-box) + css_classes - title = capture do - .gpg-popover-title + .gpg-popover-status = title - content = capture do .clearfix = content - GPG key ID: + GPG Key ID: = signature.gpg_key_primary_keyid - = link_to('Learn about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') + = 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 bottom', title: title, content: content } } +%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } = label diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml index a94fe9ef9a1..d51b99fe591 100644 --- a/app/views/projects/commit/_valid_signature_badge.html.haml +++ b/app/views/projects/commit/_valid_signature_badge.html.haml @@ -1,18 +1,18 @@ - title = capture do %i{ class: 'fa fa-check-circle gpg-popover-icon valid', 'aria-hidden' => 'true' } - This commit was signed with a verified signature. + %div + This commit was signed with a verified signature. - content = capture do - gpg_key = signature.gpg_key = link_to user_path(gpg_key.user), class: 'gpg-popover-user-link' do - .gpg-popover-avatar + %div = user_avatar_without_link(user: signature.gpg_key.user, size: 32) - .gpg-popover-username - = gpg_key.user.username - - %div= gpg_key.user.name + %div + %strong= gpg_key.user.username + %div= gpg_key.user.name - locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] } diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 709df6336fe..74eaafc9000 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -272,18 +272,18 @@ describe 'Commits' do sign_in(user) visit namespace_project_commits_path(project.namespace, project, :'signed-commits') + 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 + 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 - - 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}" + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" end end end -- cgit v1.2.1 From e63b693f28bf752f617bd0aa2f375db701d1600a Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 10 Jul 2017 13:19:50 +0200 Subject: generate gpg signature on push --- app/services/git_push_service.rb | 8 ++++ app/workers/create_gpg_signature_worker.rb | 20 ++++++++ config/sidekiq_queues.yml | 1 + spec/services/git_push_service_spec.rb | 18 +++++++ spec/workers/create_gpg_signature_worker_spec.rb | 61 ++++++++++++++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 app/workers/create_gpg_signature_worker.rb create mode 100644 spec/workers/create_gpg_signature_worker_spec.rb diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 20d1fb29289..bb7680c5054 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -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? diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb new file mode 100644 index 00000000000..6fbd6e1a3f3 --- /dev/null +++ b/app/workers/create_gpg_signature_worker.rb @@ -0,0 +1,20 @@ +class CreateGpgSignatureWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(commit_sha, project_id) + project = Project.find_by(id: project_id) + + unless project + return Rails.logger.error("CreateGpgSignatureWorker: couldn't find project with ID=#{project_id}, skipping job") + end + + commit = project.commit(commit_sha) + + unless commit + return Rails.logger.error("CreateGpgSignatureWorker: couldn't find commit with commit_sha=#{commit_sha}, skipping job") + end + + commit.signature + end +end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index cf0f5719683..7496bfa4fbb 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -30,6 +30,7 @@ - [emails_on_push, 2] - [mailers, 2] - [invalid_gpg_signature_update, 2] + - [create_gpg_signature, 2] - [upload_checksum, 1] - [use_key, 1] - [repository_fork, 1] diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index f801506f1b6..34cd44460c6 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -681,6 +681,24 @@ describe GitPushService, services: true 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 diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb new file mode 100644 index 00000000000..a23f0d6c34a --- /dev/null +++ b/spec/workers/create_gpg_signature_worker_spec.rb @@ -0,0 +1,61 @@ +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 'logs CreateGpgSignatureWorker process skipping' do + expect(Rails.logger).to receive(:error) + .with("CreateGpgSignatureWorker: couldn't find commit with commit_sha=bogus, skipping job") + + described_class.new.perform(nonexisting_commit_sha, project.id) + end + + 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 'logs CreateGpgSignatureWorker process skipping' do + expect(Rails.logger).to receive(:error) + .with("CreateGpgSignatureWorker: couldn't find project with ID=-1, skipping job") + + described_class.new.perform(anything, nonexisting_project_id) + end + + 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 -- cgit v1.2.1 From 71d884ad2d6eb4a877d2bfe163baeae0b7ddbac6 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 11 Jul 2017 13:35:57 +0200 Subject: don't use assignment in if condition --- app/workers/invalid_gpg_signature_update_worker.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb index 277dd604aa8..c0bec3c9689 100644 --- a/app/workers/invalid_gpg_signature_update_worker.rb +++ b/app/workers/invalid_gpg_signature_update_worker.rb @@ -3,10 +3,12 @@ class InvalidGpgSignatureUpdateWorker include DedicatedSidekiqQueue def perform(gpg_key_id) - if gpg_key = GpgKey.find_by(id: gpg_key_id) - Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run - else - Rails.logger.error("InvalidGpgSignatureUpdateWorker: couldn't find gpg_key with ID=#{gpg_key_id}, skipping job") + gpg_key = GpgKey.find_by(id: gpg_key_id) + + unless gpg_key + return Rails.logger.error("InvalidGpgSignatureUpdateWorker: couldn't find gpg_key with ID=#{gpg_key_id}, skipping job") end + + Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run end end -- cgit v1.2.1 From e5c9c714bd932ebb8d458c2d52535582eb48b9ee Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 11 Jul 2017 13:47:31 +0200 Subject: add notfound icon (question mark) --- app/views/shared/icons/_icon_status_notfound_borderless.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/views/shared/icons/_icon_status_notfound_borderless.svg diff --git a/app/views/shared/icons/_icon_status_notfound_borderless.svg b/app/views/shared/icons/_icon_status_notfound_borderless.svg new file mode 100644 index 00000000000..e58bd264ef8 --- /dev/null +++ b/app/views/shared/icons/_icon_status_notfound_borderless.svg @@ -0,0 +1 @@ + -- cgit v1.2.1 From f1e6a9c8323ec0a918aa9a170903d1d6ac7af5e6 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 11 Jul 2017 14:12:06 +0200 Subject: use svg icons for gpg popovers --- app/assets/stylesheets/pages/commits.scss | 20 +++++++++++++++++--- .../commit/_invalid_signature_badge.html.haml | 3 ++- .../projects/commit/_valid_signature_badge.html.haml | 3 ++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 62a1296f77e..4c66515581a 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -304,16 +304,30 @@ } .gpg-popover-icon { - font-size: 35px; // same margin as .s32.avatar margin-right: $btn-side-margin; &.valid { - color: $brand-success; + svg { + border: 1px solid $brand-success; + + fill: $brand-success; + } } &.invalid { - color: $gray; + svg { + border: 1px solid $gray; + + fill: $gray; + } + } + + svg { + width: 32px; + height: 32px; + border-radius: 50%; + vertical-align: middle; } } diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml index 0e94d0b2d59..3a73aae9d95 100644 --- a/app/views/projects/commit/_invalid_signature_badge.html.haml +++ b/app/views/projects/commit/_invalid_signature_badge.html.haml @@ -1,5 +1,6 @@ - title = capture do - %i{ class: 'fa fa-question-circle gpg-popover-icon invalid', 'aria-hidden' => 'true' } + .gpg-popover-icon.invalid + = render 'shared/icons/icon_status_notfound_borderless.svg' %div This commit was signed with an unverified signature. diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml index d51b99fe591..4db62418d0d 100644 --- a/app/views/projects/commit/_valid_signature_badge.html.haml +++ b/app/views/projects/commit/_valid_signature_badge.html.haml @@ -1,5 +1,6 @@ - title = capture do - %i{ class: 'fa fa-check-circle gpg-popover-icon valid', 'aria-hidden' => 'true' } + .gpg-popover-icon.valid + = render 'shared/icons/icon_status_success_borderless.svg' %div This commit was signed with a verified signature. -- cgit v1.2.1 From 111edaa9f75f402cc18c2bec5cab9aa6615d9c4d Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 11 Jul 2017 14:25:16 +0200 Subject: use lighter gray for unverified gpg signature --- app/assets/stylesheets/pages/commits.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 4c66515581a..41c44a8560d 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -292,7 +292,7 @@ &.invalid { color: $gray; - border: 1px solid $gray; + border: 1px solid $common-gray-light; } } @@ -317,9 +317,9 @@ &.invalid { svg { - border: 1px solid $gray; + border: 1px solid $common-gray-light; - fill: $gray; + fill: $common-gray-light; } } -- cgit v1.2.1 From 027309eb2ae54614a2ee1a0ca9e4cea76a86b94b Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 12 Jul 2017 07:59:28 +0200 Subject: user may now revoke a gpg key other than just removing a key, which doesn't affect the verified state of a commit, revoking a key unverifies all signed commits. --- app/controllers/profiles/gpg_keys_controller.rb | 10 +++++++++ app/models/gpg_key.rb | 11 ++++++++++ app/views/profiles/gpg_keys/_key.html.haml | 8 ++++++-- config/routes/profile.rb | 6 +++++- doc/workflow/gpg_signed_commits/index.md | 27 +++++++++++++++++++++++++ spec/features/profiles/gpg_keys_spec.rb | 16 +++++++++++++++ spec/models/gpg_key_spec.rb | 27 +++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 3 deletions(-) diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb index b04c14a6993..3e75247769d 100644 --- a/app/controllers/profiles/gpg_keys_controller.rb +++ b/app/controllers/profiles/gpg_keys_controller.rb @@ -25,6 +25,16 @@ class Profiles::GpgKeysController < Profiles::ApplicationController end end + def revoke + @gpp_key = current_user.gpg_keys.find(params[:id]) + @gpp_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 diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 050245bd502..1977023536e 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -58,6 +58,17 @@ class GpgKey < ActiveRecord::Base InvalidGpgSignatureUpdateWorker.perform_async(self.id) end + def revoke + GpgSignature.where(gpg_key: self, valid_signature: true).find_each do |gpg_signature| + gpg_signature.update_attributes!( + gpg_key: nil, + valid_signature: false + ) + end + + destroy + end + private def extract_fingerprint diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index b4b9aa07190..86e2510d22f 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -3,13 +3,17 @@ = icon 'key', class: "settings-list-icon hidden-xs" .key-list-item-info - key.emails_with_verified_status.map do |email, verified| + = email = verified_email_badge(email, verified) .description - = key.fingerprint + %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?' }, method: :delete, class: "btn btn-transparent prepend-left-10" do + = 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 diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 00388b9c0cd..3e4e6111ab8 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -23,7 +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] + 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 diff --git a/doc/workflow/gpg_signed_commits/index.md b/doc/workflow/gpg_signed_commits/index.md index f7f5492c35a..7d5762d2b9d 100644 --- a/doc/workflow/gpg_signed_commits/index.md +++ b/doc/workflow/gpg_signed_commits/index.md @@ -42,6 +42,33 @@ For a signature to be verified two prerequisites need to be met: 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 diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index 350126523b0..6edc482b47e 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -39,4 +39,20 @@ feature 'Profile > GPG Keys' do 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 diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index ffbf8760e86..ddd0bbfb9ba 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -95,4 +95,31 @@ describe GpgKey do 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 -- cgit v1.2.1 From 506836a695ae40ff200add21c639f3d13aaee9e9 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 13 Jul 2017 10:53:19 +0200 Subject: unify commit signature colors with pipeline status --- app/assets/stylesheets/framework/mixins.scss | 26 ++++++++++++++++++++++++++ app/assets/stylesheets/pages/commits.scss | 8 ++++---- app/assets/stylesheets/pages/status.scss | 21 +-------------------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 3a98332e46c..6f91d11b369 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -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); +} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 41c44a8560d..cd9f2d787c5 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -284,15 +284,15 @@ } } + .gpg-status-box { &.valid { - color: $brand-success; - border: 1px solid $brand-success; + @include green-status-color; } &.invalid { - color: $gray; - border: 1px solid $common-gray-light; + @include status-color($gray-dark, $gray, $common-gray-dark); + border-color: $common-gray-light; } } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 67ad1ae60af..36f622db136 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -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, -- cgit v1.2.1 From cd01e82873b3cd471203dbf557c71571fd683d16 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 13 Jul 2017 15:22:15 +0200 Subject: store gpg user name and email on the signature --- app/models/gpg_key.rb | 16 ++++++++----- ...4235_add_gpg_key_user_info_to_gpg_signatures.rb | 11 +++++++++ db/schema.rb | 2 ++ lib/gitlab/gpg.rb | 6 +++-- lib/gitlab/gpg/commit.rb | 21 ++++++++++++----- spec/lib/gitlab/gpg/commit_spec.rb | 6 +++++ spec/lib/gitlab/gpg_spec.rb | 14 +++++++----- spec/models/gpg_key_spec.rb | 26 +++++++++++++++++++--- spec/support/gpg_helpers.rb | 8 +++++++ 9 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 db/migrate/20170713104235_add_gpg_key_user_info_to_gpg_signatures.rb diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 1977023536e..31a25f3e2f0 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -37,15 +37,21 @@ class GpgKey < ActiveRecord::Base write_attribute(:key, value) end - def emails - @emails ||= Gitlab::Gpg.emails_from_key(key) + 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 - emails.map do |email| + user_infos.map do |user_info| [ - email, - email == user.email + user_info[:email], + user_info[:email] == user.email ] end.to_h end diff --git a/db/migrate/20170713104235_add_gpg_key_user_info_to_gpg_signatures.rb b/db/migrate/20170713104235_add_gpg_key_user_info_to_gpg_signatures.rb new file mode 100644 index 00000000000..0e51a86e64c --- /dev/null +++ b/db/migrate/20170713104235_add_gpg_key_user_info_to_gpg_signatures.rb @@ -0,0 +1,11 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddGpgKeyUserInfoToGpgSignatures < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :gpg_signatures, :gpg_key_user_name, :string + add_column :gpg_signatures, :gpg_key_user_email, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 53b1e83ddab..b76a5efbbd7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -560,6 +560,8 @@ ActiveRecord::Schema.define(version: 20170725145659) do t.boolean "valid_signature" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "gpg_key_user_name" + t.string "gpg_key_user_email" end add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", using: :btree diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 582347019e5..e1d1724295a 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -32,11 +32,13 @@ module Gitlab end end - def emails_from_key(key) + def user_infos_from_key(key) using_tmp_keychain do fingerprints = CurrentKeyChain.fingerprints_from_key(key) - GPGME::Key.find(:public, fingerprints).flat_map { |raw_key| raw_key.uids.map(&:email) } + GPGME::Key.find(:public, fingerprints).flat_map do |raw_key| + raw_key.uids.map { |uid| { name: uid.name, email: uid.email } } + end end end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 50e8d71bb13..55428b85207 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -26,10 +26,7 @@ module Gitlab def update_signature!(cached_signature) using_keychain do |gpg_key| - cached_signature.update_attributes!( - valid_signature: gpg_signature_valid_signature_value(gpg_key), - gpg_key: gpg_key - ) + cached_signature.update_attributes!(attributes(gpg_key)) end end @@ -59,18 +56,30 @@ module Gitlab end def create_cached_signature!(gpg_key) - GpgSignature.create!( + 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 diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 661956b7bb7..ddb8dd9f0f4 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -32,6 +32,8 @@ RSpec.describe Gitlab::Gpg::Commit do 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 @@ -67,6 +69,8 @@ RSpec.describe Gitlab::Gpg::Commit do 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 @@ -102,6 +106,8 @@ RSpec.describe Gitlab::Gpg::Commit do 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 diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index ebb7720eaea..8041518117d 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -28,16 +28,18 @@ describe Gitlab::Gpg do end end - describe '.emails_from_key' do - it 'returns the emails' do - expect( - described_class.emails_from_key(GpgHelpers::User1.public_key) - ).to eq GpgHelpers::User1.emails + 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.emails_from_key('bogus') + described_class.user_infos_from_key('bogus') ).to eq [] end end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index ddd0bbfb9ba..06bdbb59a11 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -46,11 +46,31 @@ describe GpgKey do end end - describe '#emails' do - it 'returns the emails from the gpg key' do + 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) - expect(gpg_key.emails).to eq GpgHelpers::User1.emails + 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 diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb index f9128a629f2..96ea6f28b30 100644 --- a/spec/support/gpg_helpers.rb +++ b/spec/support/gpg_helpers.rb @@ -98,6 +98,10 @@ module GpgHelpers '5F7EA3981A5845B141ABD522CCFBE19F00AC8B1D' end + def names + ['Nannie Bernhard'] + end + def emails ['nannie.bernhard@example.com'] end @@ -187,6 +191,10 @@ module GpgHelpers '6D494CA6FC90C0CAE0910E42BF9D925F911EFD65' end + def names + ['Bette Cartwright', 'Bette Cartwright'] + end + def emails ['bette.cartwright@example.com', 'bette.cartwright@example.net'] end -- cgit v1.2.1 From c52718332cb723cc4b3035c17eec9eeb9926c8cf Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 13 Jul 2017 16:04:19 +0200 Subject: show gpg key's user info when no profile exists --- .../commit/_valid_signature_badge.html.haml | 24 ++++++++++++++++------ spec/features/commits_spec.rb | 23 ++++++++++++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml index 4db62418d0d..5a451cb4055 100644 --- a/app/views/projects/commit/_valid_signature_badge.html.haml +++ b/app/views/projects/commit/_valid_signature_badge.html.haml @@ -6,14 +6,26 @@ - 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 - = link_to user_path(gpg_key.user), class: 'gpg-popover-user-link' do - %div - = user_avatar_without_link(user: signature.gpg_key.user, size: 32) + - 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.username - %div= gpg_key.user.name + %div + %strong= gpg_key.user.username + %div= gpg_key.user.name + - 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'] } diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 74eaafc9000..b6b0cc7e1d3 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -263,21 +263,25 @@ describe 'Commits' do end it 'shows popover badges', :js do - user = create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard' - project.team << [user, :master] + 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: user + create :gpg_key, key: GpgHelpers::User1.public_key, user: gpg_user end + user = create :user + project.team << [user, :master] + sign_in(user) visit namespace_project_commits_path(project.namespace, 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.' @@ -285,6 +289,19 @@ describe 'Commits' do 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 namespace_project_commits_path(project.namespace, 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 -- cgit v1.2.1 From ccf3ed4351ce45204035169c67ee7f3c01b05e81 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 13 Jul 2017 16:08:14 +0200 Subject: swap user's name and the user's username --- app/views/projects/commit/_valid_signature_badge.html.haml | 4 ++-- spec/features/commits_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml index 5a451cb4055..db1a41bbf64 100644 --- a/app/views/projects/commit/_valid_signature_badge.html.haml +++ b/app/views/projects/commit/_valid_signature_badge.html.haml @@ -16,8 +16,8 @@ = user_avatar_without_link(user: user, size: 32) %div - %strong= gpg_key.user.username - %div= gpg_key.user.name + %strong= gpg_key.user.name + %div @#{gpg_key.user.username} - else = mail_to user_email do %div diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index b6b0cc7e1d3..9bd4b478cce 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -285,8 +285,8 @@ describe 'Commits' do 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 '@nannie.bernhard' expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" end -- cgit v1.2.1 From a03a6ff326300daafbd67fd32eaaa08a4b649395 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 13 Jul 2017 16:51:16 +0200 Subject: display gpg key in the popover with monospace font --- app/views/projects/commit/_signature_badge.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index 8e09a9333aa..d6ece085f18 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -9,7 +9,8 @@ = content GPG Key ID: - = signature.gpg_key_primary_keyid + %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') -- cgit v1.2.1 From 312dc89a44642050a2224c1b780054828c819fd6 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 13 Jul 2017 16:52:34 +0200 Subject: nicer email badges on the profile gpg page --- app/assets/stylesheets/pages/profile.scss | 23 ++++++++++++++++++++++ app/helpers/badges_helper.rb | 11 ----------- .../profiles/gpg_keys/_email_with_badge.html.haml | 8 ++++++++ app/views/profiles/gpg_keys/_key.html.haml | 3 +-- 4 files changed, 32 insertions(+), 13 deletions(-) delete mode 100644 app/helpers/badges_helper.rb create mode 100644 app/views/profiles/gpg_keys/_email_with_badge.html.haml diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 22672614e0d..14ad06b0ac2 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -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); + } + } +} diff --git a/app/helpers/badges_helper.rb b/app/helpers/badges_helper.rb deleted file mode 100644 index e1c8927ab54..00000000000 --- a/app/helpers/badges_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module BadgesHelper - def verified_email_badge(email, verified) - css_classes = %w(btn btn-xs disabled) - - css_classes << 'btn-success' if verified - - content_tag 'span', class: css_classes do - "#{email} #{verified ? 'Verified' : 'Unverified'}" - end - end -end diff --git a/app/views/profiles/gpg_keys/_email_with_badge.html.haml b/app/views/profiles/gpg_keys/_email_with_badge.html.haml new file mode 100644 index 00000000000..5f7844584e1 --- /dev/null +++ b/app/views/profiles/gpg_keys/_email_with_badge.html.haml @@ -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 diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index 86e2510d22f..b04981f90e3 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -3,8 +3,7 @@ = icon 'key', class: "settings-list-icon hidden-xs" .key-list-item-info - key.emails_with_verified_status.map do |email, verified| - = email - = verified_email_badge(email, verified) + = render partial: 'email_with_badge', locals: { email: email, verified: verified } .description %code= key.fingerprint -- cgit v1.2.1 From 8c8a9e6d3fcb529e95d76dc9a7d4e37542a2036f Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Sat, 15 Jul 2017 15:25:21 +0200 Subject: merge migrations to 1 single create per table also: * reorder table columns * no need for `add_concurrent_index` * no need for explicit index removal on `#down` --- db/migrate/20170222111732_create_gpg_keys.rb | 9 +++++-- ...20170613103429_add_primary_keyid_to_gpg_keys.rb | 17 ------------- db/migrate/20170613154149_create_gpg_signatures.rb | 29 ++++++++-------------- ...4235_add_gpg_key_user_info_to_gpg_signatures.rb | 11 -------- db/schema.rb | 14 +++++------ 5 files changed, 25 insertions(+), 55 deletions(-) delete mode 100644 db/migrate/20170613103429_add_primary_keyid_to_gpg_keys.rb delete mode 100644 db/migrate/20170713104235_add_gpg_key_user_info_to_gpg_signatures.rb diff --git a/db/migrate/20170222111732_create_gpg_keys.rb b/db/migrate/20170222111732_create_gpg_keys.rb index 1b8b7a91fe1..55dc730e884 100644 --- a/db/migrate/20170222111732_create_gpg_keys.rb +++ b/db/migrate/20170222111732_create_gpg_keys.rb @@ -3,11 +3,16 @@ class CreateGpgKeys < ActiveRecord::Migration def change create_table :gpg_keys do |t| + t.timestamps_with_timezone null: false + + t.references :user, index: true, foreign_key: true + t.string :fingerprint + t.string :primary_keyid + t.text :key - t.references :user, index: true, foreign_key: true - t.timestamps_with_timezone null: false + t.index :primary_keyid end end end diff --git a/db/migrate/20170613103429_add_primary_keyid_to_gpg_keys.rb b/db/migrate/20170613103429_add_primary_keyid_to_gpg_keys.rb deleted file mode 100644 index 13f0500971b..00000000000 --- a/db/migrate/20170613103429_add_primary_keyid_to_gpg_keys.rb +++ /dev/null @@ -1,17 +0,0 @@ -class AddPrimaryKeyidToGpgKeys < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - disable_ddl_transaction! - - def up - add_column :gpg_keys, :primary_keyid, :string - add_concurrent_index :gpg_keys, :primary_keyid - end - - def down - remove_concurrent_index :gpg_keys, :primary_keyid if index_exists?(:gpg_keys, :primary_keyid) - remove_column :gpg_keys, :primary_keyid, :string - end -end diff --git a/db/migrate/20170613154149_create_gpg_signatures.rb b/db/migrate/20170613154149_create_gpg_signatures.rb index 72560cdb6d0..515c1413cf4 100644 --- a/db/migrate/20170613154149_create_gpg_signatures.rb +++ b/db/migrate/20170613154149_create_gpg_signatures.rb @@ -1,29 +1,22 @@ class CreateGpgSignatures < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - DOWNTIME = false - disable_ddl_transaction! - - def up + def change create_table :gpg_signatures do |t| - t.string :commit_sha + t.timestamps_with_timezone null: false + t.references :project, index: true, foreign_key: true t.references :gpg_key, index: true, foreign_key: true - t.string :gpg_key_primary_keyid - t.boolean :valid_signature - t.timestamps_with_timezone null: false - end - - add_concurrent_index :gpg_signatures, :commit_sha - add_concurrent_index :gpg_signatures, :gpg_key_primary_keyid - end + t.boolean :valid_signature - def down - remove_concurrent_index :gpg_signatures, :commit_sha if index_exists?(:gpg_signatures, :commit_sha) - remove_concurrent_index :gpg_signatures, :gpg_key_primary_keyid if index_exists?(:gpg_signatures, :gpg_key_primary_keyid) + t.string :commit_sha + t.string :gpg_key_primary_keyid + t.string :gpg_key_user_name + t.string :gpg_key_user_email - drop_table :gpg_signatures + t.index :commit_sha + t.index :gpg_key_primary_keyid + end end end diff --git a/db/migrate/20170713104235_add_gpg_key_user_info_to_gpg_signatures.rb b/db/migrate/20170713104235_add_gpg_key_user_info_to_gpg_signatures.rb deleted file mode 100644 index 0e51a86e64c..00000000000 --- a/db/migrate/20170713104235_add_gpg_key_user_info_to_gpg_signatures.rb +++ /dev/null @@ -1,11 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class AddGpgKeyUserInfoToGpgSignatures < ActiveRecord::Migration - DOWNTIME = false - - def change - add_column :gpg_signatures, :gpg_key_user_name, :string - add_column :gpg_signatures, :gpg_key_user_email, :string - end -end diff --git a/db/schema.rb b/db/schema.rb index b76a5efbbd7..f413aaa41cd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -541,25 +541,25 @@ 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.string "fingerprint" - t.text "key" - t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "user_id" + t.string "fingerprint" t.string "primary_keyid" + t.text "key" end add_index "gpg_keys", ["primary_keyid"], name: "index_gpg_keys_on_primary_keyid", 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.string "commit_sha" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "project_id" t.integer "gpg_key_id" - t.string "gpg_key_primary_keyid" t.boolean "valid_signature" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "commit_sha" + t.string "gpg_key_primary_keyid" t.string "gpg_key_user_name" t.string "gpg_key_user_email" end -- cgit v1.2.1 From 8e0c33ed1337e3614fe87d9d0c1eb64af90cc61a Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 20 Jul 2017 15:44:15 +0200 Subject: use ShaAttribute for gpg table columns --- app/models/gpg_key.rb | 5 +++++ app/models/gpg_signature.rb | 5 +++++ app/views/profiles/gpg_keys/_key.html.haml | 2 +- app/views/projects/commit/_signature_badge.html.haml | 2 +- db/migrate/20170222111732_create_gpg_keys.rb | 4 ++-- db/migrate/20170613154149_create_gpg_signatures.rb | 5 +++-- db/migrate/limits_to_mysql.rb | 4 ++++ db/schema.rb | 8 ++++---- 8 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 31a25f3e2f0..da2875a8851 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -1,6 +1,11 @@ class GpgKey < ActiveRecord::Base KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze + include ShaAttribute + + sha_attribute :primary_keyid + sha_attribute :fingerprint + belongs_to :user has_many :gpg_signatures, dependent: :nullify diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 0ef335bf8a9..9ac89f0bbbf 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -1,4 +1,9 @@ class GpgSignature < ActiveRecord::Base + include ShaAttribute + + sha_attribute :commit_sha + sha_attribute :gpg_key_primary_keyid + belongs_to :project belongs_to :gpg_key diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index b04981f90e3..d625aaea467 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -6,7 +6,7 @@ = render partial: 'email_with_badge', locals: { email: email, verified: verified } .description - %code= key.fingerprint + %code= key.fingerprint.upcase .pull-right %span.key-created-at created #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index d6ece085f18..e79360a36e5 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -9,7 +9,7 @@ = content GPG Key ID: - %span.monospace= signature.gpg_key_primary_keyid + %span.monospace= signature.gpg_key_primary_keyid.upcase = link_to('Learn more about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') diff --git a/db/migrate/20170222111732_create_gpg_keys.rb b/db/migrate/20170222111732_create_gpg_keys.rb index 55dc730e884..7591238311f 100644 --- a/db/migrate/20170222111732_create_gpg_keys.rb +++ b/db/migrate/20170222111732_create_gpg_keys.rb @@ -7,8 +7,8 @@ class CreateGpgKeys < ActiveRecord::Migration t.references :user, index: true, foreign_key: true - t.string :fingerprint - t.string :primary_keyid + t.binary :primary_keyid, limit: Gitlab::Database.mysql? ? 20 : nil + t.binary :fingerprint, limit: Gitlab::Database.mysql? ? 20 : nil t.text :key diff --git a/db/migrate/20170613154149_create_gpg_signatures.rb b/db/migrate/20170613154149_create_gpg_signatures.rb index 515c1413cf4..c5478551e11 100644 --- a/db/migrate/20170613154149_create_gpg_signatures.rb +++ b/db/migrate/20170613154149_create_gpg_signatures.rb @@ -10,8 +10,9 @@ class CreateGpgSignatures < ActiveRecord::Migration t.boolean :valid_signature - t.string :commit_sha - t.string :gpg_key_primary_keyid + t.binary :commit_sha, limit: Gitlab::Database.mysql? ? 20 : nil + t.binary :gpg_key_primary_keyid, limit: Gitlab::Database.mysql? ? 20 : nil + t.string :gpg_key_user_name t.string :gpg_key_user_email diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index be3501c4c2e..de1288e6410 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -8,5 +8,9 @@ class LimitsToMysql < ActiveRecord::Migration change_column :snippets, :content, :text, limit: 2147483647 change_column :notes, :st_diff, :text, limit: 2147483647 change_column :events, :data, :text, limit: 2147483647 + change_column :gpg_keys, :primary_keyid, :binary, limit: 20 + change_column :gpg_keys, :fingerprint, :binary, limit: 20 + change_column :gpg_signatures, :commit_sha, :binary, limit: 20 + change_column :gpg_signatures, :gpg_key_primary_keyid, :binary, limit: 20 end end diff --git a/db/schema.rb b/db/schema.rb index f413aaa41cd..68b5963ec14 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -544,8 +544,8 @@ ActiveRecord::Schema.define(version: 20170725145659) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "user_id" - t.string "fingerprint" - t.string "primary_keyid" + t.binary "primary_keyid" + t.binary "fingerprint" t.text "key" end @@ -558,8 +558,8 @@ ActiveRecord::Schema.define(version: 20170725145659) do t.integer "project_id" t.integer "gpg_key_id" t.boolean "valid_signature" - t.string "commit_sha" - t.string "gpg_key_primary_keyid" + t.binary "commit_sha" + t.binary "gpg_key_primary_keyid" t.string "gpg_key_user_name" t.string "gpg_key_user_email" end -- cgit v1.2.1 From 895efdfbcfe6082709943767dc8b3ebf399e1283 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 20 Jul 2017 16:11:53 +0200 Subject: use text instead of string for db columns --- db/migrate/20170613154149_create_gpg_signatures.rb | 4 ++-- db/schema.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/db/migrate/20170613154149_create_gpg_signatures.rb b/db/migrate/20170613154149_create_gpg_signatures.rb index c5478551e11..d1fe4b96a40 100644 --- a/db/migrate/20170613154149_create_gpg_signatures.rb +++ b/db/migrate/20170613154149_create_gpg_signatures.rb @@ -13,8 +13,8 @@ class CreateGpgSignatures < ActiveRecord::Migration t.binary :commit_sha, limit: Gitlab::Database.mysql? ? 20 : nil t.binary :gpg_key_primary_keyid, limit: Gitlab::Database.mysql? ? 20 : nil - t.string :gpg_key_user_name - t.string :gpg_key_user_email + t.text :gpg_key_user_name + t.text :gpg_key_user_email t.index :commit_sha t.index :gpg_key_primary_keyid diff --git a/db/schema.rb b/db/schema.rb index 68b5963ec14..6fa8be79156 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -560,8 +560,8 @@ ActiveRecord::Schema.define(version: 20170725145659) do t.boolean "valid_signature" t.binary "commit_sha" t.binary "gpg_key_primary_keyid" - t.string "gpg_key_user_name" - t.string "gpg_key_user_email" + 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", using: :btree -- cgit v1.2.1 From 786b5a5991930bb838767a4ed6eed2a67e517e82 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 20 Jul 2017 16:18:02 +0200 Subject: use short project path helpers --- spec/features/commits_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 9bd4b478cce..729d83968d3 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -215,7 +215,7 @@ describe 'Commits' do sign_in(user) - visit namespace_project_commits_path(project.namespace, project, :'signed-commits') + visit project_commits_path(project, :'signed-commits') within '#commits-list' do expect(page).to have_content 'Unverified' @@ -228,7 +228,7 @@ describe 'Commits' do user.update_attributes!(email: GpgHelpers::User1.emails.first) end - visit namespace_project_commits_path(project.namespace, project, :'signed-commits') + visit project_commits_path(project, :'signed-commits') within '#commits-list' do expect(page).to have_content 'Unverified' @@ -242,7 +242,7 @@ describe 'Commits' do sign_in(user) - visit namespace_project_commits_path(project.namespace, project, :'signed-commits') + visit project_commits_path(project, :'signed-commits') within '#commits-list' do expect(page).to have_content 'Unverified' @@ -254,7 +254,7 @@ describe 'Commits' do create :gpg_key, key: GpgHelpers::User1.public_key, user: user end - visit namespace_project_commits_path(project.namespace, project, :'signed-commits') + visit project_commits_path(project, :'signed-commits') within '#commits-list' do expect(page).to have_content 'Unverified' @@ -272,7 +272,7 @@ describe 'Commits' do project.team << [user, :master] sign_in(user) - visit namespace_project_commits_path(project.namespace, project, :'signed-commits') + visit project_commits_path(project, :'signed-commits') # unverified signature click_on 'Unverified', match: :first @@ -293,7 +293,7 @@ describe 'Commits' do # verified and the gpg user's profile doesn't exist anymore gpg_user.destroy! - visit namespace_project_commits_path(project.namespace, project, :'signed-commits') + visit project_commits_path(project, :'signed-commits') click_on 'Verified' within '.popover' do -- cgit v1.2.1 From 57ccff8ea41aa2366f40b29187d3b8d1217264e0 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 20 Jul 2017 16:33:44 +0200 Subject: use db's on_delete instead of has_many :dependent --- app/models/gpg_key.rb | 2 +- app/models/user.rb | 2 +- db/migrate/20170222111732_create_gpg_keys.rb | 2 +- db/migrate/20170613154149_create_gpg_signatures.rb | 4 ++-- db/schema.rb | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index da2875a8851..47ebfc9d234 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -7,7 +7,7 @@ class GpgKey < ActiveRecord::Base sha_attribute :fingerprint belongs_to :user - has_many :gpg_signatures, dependent: :nullify + has_many :gpg_signatures validates :user, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index 03a76f4fa23..6e66c587a1f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -76,7 +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, dependent: :destroy + has_many :gpg_keys has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/db/migrate/20170222111732_create_gpg_keys.rb b/db/migrate/20170222111732_create_gpg_keys.rb index 7591238311f..072c0819b2f 100644 --- a/db/migrate/20170222111732_create_gpg_keys.rb +++ b/db/migrate/20170222111732_create_gpg_keys.rb @@ -5,7 +5,7 @@ class CreateGpgKeys < ActiveRecord::Migration create_table :gpg_keys do |t| t.timestamps_with_timezone null: false - t.references :user, index: true, foreign_key: true + t.references :user, index: true, foreign_key: { on_delete: :cascade } t.binary :primary_keyid, limit: Gitlab::Database.mysql? ? 20 : nil t.binary :fingerprint, limit: Gitlab::Database.mysql? ? 20 : nil diff --git a/db/migrate/20170613154149_create_gpg_signatures.rb b/db/migrate/20170613154149_create_gpg_signatures.rb index d1fe4b96a40..db86170776d 100644 --- a/db/migrate/20170613154149_create_gpg_signatures.rb +++ b/db/migrate/20170613154149_create_gpg_signatures.rb @@ -5,8 +5,8 @@ class CreateGpgSignatures < ActiveRecord::Migration create_table :gpg_signatures do |t| t.timestamps_with_timezone null: false - t.references :project, index: true, foreign_key: true - t.references :gpg_key, index: true, foreign_key: true + 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 diff --git a/db/schema.rb b/db/schema.rb index 6fa8be79156..1a7eb2ded76 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1631,9 +1631,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" - add_foreign_key "gpg_signatures", "gpg_keys" - add_foreign_key "gpg_signatures", "projects" + 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 -- cgit v1.2.1 From c4c44c6a1bb892dc17989cef3cc9b6c23fecb2c8 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 20 Jul 2017 22:07:04 +0200 Subject: length constrain on the index, not on the column we actually don't need a limit on the column itself for MySQL to work. we need to set a length on the index. --- db/migrate/20170222111732_create_gpg_keys.rb | 6 +++--- db/migrate/20170613154149_create_gpg_signatures.rb | 8 ++++---- db/migrate/limits_to_mysql.rb | 17 +++++++++++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/db/migrate/20170222111732_create_gpg_keys.rb b/db/migrate/20170222111732_create_gpg_keys.rb index 072c0819b2f..ec619394f6a 100644 --- a/db/migrate/20170222111732_create_gpg_keys.rb +++ b/db/migrate/20170222111732_create_gpg_keys.rb @@ -7,12 +7,12 @@ class CreateGpgKeys < ActiveRecord::Migration t.references :user, index: true, foreign_key: { on_delete: :cascade } - t.binary :primary_keyid, limit: Gitlab::Database.mysql? ? 20 : nil - t.binary :fingerprint, limit: Gitlab::Database.mysql? ? 20 : nil + t.binary :primary_keyid + t.binary :fingerprint t.text :key - t.index :primary_keyid + t.index :primary_keyid, length: Gitlab::Database.mysql? ? 20 : nil end end end diff --git a/db/migrate/20170613154149_create_gpg_signatures.rb b/db/migrate/20170613154149_create_gpg_signatures.rb index db86170776d..775a9463914 100644 --- a/db/migrate/20170613154149_create_gpg_signatures.rb +++ b/db/migrate/20170613154149_create_gpg_signatures.rb @@ -10,14 +10,14 @@ class CreateGpgSignatures < ActiveRecord::Migration t.boolean :valid_signature - t.binary :commit_sha, limit: Gitlab::Database.mysql? ? 20 : nil - t.binary :gpg_key_primary_keyid, limit: Gitlab::Database.mysql? ? 20 : nil + 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 - t.index :gpg_key_primary_keyid + t.index :commit_sha, length: Gitlab::Database.mysql? ? 20 : nil + t.index :gpg_key_primary_keyid, length: Gitlab::Database.mysql? ? 20 : nil end end end diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index de1288e6410..69ada782abe 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -1,5 +1,9 @@ # rubocop:disable all +require Rails.root.join('lib/gitlab/database/migration_helpers.rb') + class LimitsToMysql < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + def up return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ @@ -8,9 +12,14 @@ class LimitsToMysql < ActiveRecord::Migration change_column :snippets, :content, :text, limit: 2147483647 change_column :notes, :st_diff, :text, limit: 2147483647 change_column :events, :data, :text, limit: 2147483647 - change_column :gpg_keys, :primary_keyid, :binary, limit: 20 - change_column :gpg_keys, :fingerprint, :binary, limit: 20 - change_column :gpg_signatures, :commit_sha, :binary, limit: 20 - change_column :gpg_signatures, :gpg_key_primary_keyid, :binary, limit: 20 + + [ + [:gpg_keys, :primary_keyid], + [:gpg_signatures, :commit_sha], + [:gpg_signatures, :gpg_key_primary_keyid] + ].each do |table_name, column_name| + remove_index table_name, column_name if index_exists?(table_name, column_name) + add_concurrent_index table_name, column_name, length: 20 + end end end -- cgit v1.2.1 From eda001565c5afbf6e2eb9b8b5cf4fa9d6525ed71 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 09:40:23 +0200 Subject: fetch gpg signature badges by ajax --- app/assets/javascripts/dispatcher.js | 4 +++ app/assets/javascripts/gpg_badges.js | 15 ++++++++ app/controllers/projects/commits_controller.rb | 40 ++++++++++++++++------ app/helpers/commits_helper.rb | 4 +++ app/models/commit.rb | 8 ++++- .../projects/commit/_ajax_signature.html.haml | 3 ++ .../projects/commit/_signature_badge.html.haml | 2 +- app/views/projects/commits/_commit.html.haml | 7 +++- app/views/projects/commits/show.html.haml | 2 +- config/routes/repository.rb | 2 ++ spec/features/commits_spec.rb | 4 +-- 11 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/gpg_badges.js create mode 100644 app/views/projects/commit/_ajax_signature.html.haml diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 1dc6edacfed..f2f814b9e18 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -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; diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js new file mode 100644 index 00000000000..1c379e9bb67 --- /dev/null +++ b/app/assets/javascripts/gpg_badges.js @@ -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); + }); + }); + } +} diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 37b5a6e9d48..2de9900d449 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -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 diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index d08e346d605..69220a1c0f6 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -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 diff --git a/app/models/commit.rb b/app/models/commit.rb index 35593d53cbc..7940733f557 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -237,9 +237,11 @@ class Commit def signature return @signature if defined?(@signature) - @signature = Gitlab::Gpg::Commit.new(self).signature + @signature = gpg_commit.signature end + delegate :has_signature?, to: :gpg_commit + def revert_branch_name "revert-#{short_id}" end @@ -388,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 diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml new file mode 100644 index 00000000000..22674b671c9 --- /dev/null +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -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 diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index e79360a36e5..51f04a11712 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -1,4 +1,4 @@ -- css_classes = %w(btn status-box gpg-status-box) + css_classes +- css_classes = commit_signature_badge_classes(css_classes) - title = capture do .gpg-popover-status diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index b7f18d44838..12b73ecdf13 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -39,7 +39,12 @@ .commit-actions.hidden-xs - if commit.status(ref) = render_commit_status(commit, ref: ref) - = render partial: 'projects/commit/signature', object: commit.signature + + - 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) diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 844ebb65148..bd2d900997e 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -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 diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 11911636fa7..edcf3ddf57b 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -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 diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 729d83968d3..87a0dc328a6 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -204,7 +204,7 @@ describe 'Commits' do end end - describe 'GPG signed commits' do + 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] @@ -262,7 +262,7 @@ describe 'Commits' do end end - it 'shows popover badges', :js do + 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 -- cgit v1.2.1 From ce4e0837c4ce11ad31c7be487d08bf44d961ec6f Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 10:02:13 +0200 Subject: mysql hack: set length for binary indexes --- .../mysql_set_length_for_binary_indexes.rb | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 config/initializers/mysql_set_length_for_binary_indexes.rb diff --git a/config/initializers/mysql_set_length_for_binary_indexes.rb b/config/initializers/mysql_set_length_for_binary_indexes.rb new file mode 100644 index 00000000000..b5c6e39f6a8 --- /dev/null +++ b/config/initializers/mysql_set_length_for_binary_indexes.rb @@ -0,0 +1,25 @@ +# 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. + +if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) + module ActiveRecord + module ConnectionAdapters + class Mysql2Adapter < AbstractMysqlAdapter + alias_method :__gitlab_add_index2, :add_index + + 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 + + __gitlab_add_index2(table_name, column_names, options) + end + end + end + end +end -- cgit v1.2.1 From f86580c075f50b78517283febca012afcc8b6211 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 16:02:30 +0200 Subject: no more more :length due to mysql set_index hack --- db/migrate/limits_to_mysql.rb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index 69ada782abe..be3501c4c2e 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -1,9 +1,5 @@ # rubocop:disable all -require Rails.root.join('lib/gitlab/database/migration_helpers.rb') - class LimitsToMysql < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - def up return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ @@ -12,14 +8,5 @@ class LimitsToMysql < ActiveRecord::Migration change_column :snippets, :content, :text, limit: 2147483647 change_column :notes, :st_diff, :text, limit: 2147483647 change_column :events, :data, :text, limit: 2147483647 - - [ - [:gpg_keys, :primary_keyid], - [:gpg_signatures, :commit_sha], - [:gpg_signatures, :gpg_key_primary_keyid] - ].each do |table_name, column_name| - remove_index table_name, column_name if index_exists?(table_name, column_name) - add_concurrent_index table_name, column_name, length: 20 - end end end -- cgit v1.2.1 From 98531fc2487f8d4d7de47fe9a1d60c10d1f1d9ba Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 16:23:52 +0200 Subject: upcase in the model instead of in the view --- app/models/gpg_key.rb | 8 ++++++++ app/models/gpg_signature.rb | 4 ++++ app/views/profiles/gpg_keys/_key.html.haml | 2 +- app/views/projects/commit/_signature_badge.html.haml | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 47ebfc9d234..0d35baa7ade 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -37,6 +37,14 @@ class GpgKey < ActiveRecord::Base 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) value.strip! unless value.blank? write_attribute(:key, value) diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 9ac89f0bbbf..cb69106183d 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -11,6 +11,10 @@ class GpgSignature < ActiveRecord::Base validates :project, presence: true validates :gpg_key_primary_keyid, presence: true + def gpg_key_primary_keyid + super&.upcase + end + def commit project.commit(commit_sha) end diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index d625aaea467..b04981f90e3 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -6,7 +6,7 @@ = render partial: 'email_with_badge', locals: { email: email, verified: verified } .description - %code= key.fingerprint.upcase + %code= key.fingerprint .pull-right %span.key-created-at created #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index 51f04a11712..66f00eb5507 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -9,7 +9,7 @@ = content GPG Key ID: - %span.monospace= signature.gpg_key_primary_keyid.upcase + %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') -- cgit v1.2.1 From ecbc11a839f7a48402e912f1176735770c091829 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 16:24:22 +0200 Subject: extract setter as before_action --- app/controllers/profiles/gpg_keys_controller.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb index 3e75247769d..6779cc6ddac 100644 --- a/app/controllers/profiles/gpg_keys_controller.rb +++ b/app/controllers/profiles/gpg_keys_controller.rb @@ -1,4 +1,6 @@ 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 @@ -16,8 +18,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController end def destroy - @gpp_key = current_user.gpg_keys.find(params[:id]) - @gpp_key.destroy + @gpg_key.destroy respond_to do |format| format.html { redirect_to profile_gpg_keys_url, status: 302 } @@ -26,8 +27,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController end def revoke - @gpp_key = current_user.gpg_keys.find(params[:id]) - @gpp_key.revoke + @gpg_key.revoke respond_to do |format| format.html { redirect_to profile_gpg_keys_url, status: 302 } @@ -40,4 +40,8 @@ class Profiles::GpgKeysController < Profiles::ApplicationController 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 -- cgit v1.2.1 From 07dbd5649ad18e4473c10ef8a1a70ea863b88cc4 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 16:45:13 +0200 Subject: use Module#prepend instead of alias_method_chain --- .../mysql_set_length_for_binary_indexes.rb | 28 ++++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/config/initializers/mysql_set_length_for_binary_indexes.rb b/config/initializers/mysql_set_length_for_binary_indexes.rb index b5c6e39f6a8..de0bc5322aa 100644 --- a/config/initializers/mysql_set_length_for_binary_indexes.rb +++ b/config/initializers/mysql_set_length_for_binary_indexes.rb @@ -2,24 +2,20 @@ # MySQL adapter apply a length of 20. Otherwise MySQL can't create an index on # binary columns. -if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) - module ActiveRecord - module ConnectionAdapters - class Mysql2Adapter < AbstractMysqlAdapter - alias_method :__gitlab_add_index2, :add_index - - 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 } +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 - - __gitlab_add_index2(table_name, column_names, options) - end + 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 -- cgit v1.2.1 From 14551424c9fd3a9401559e6d2da34be8d1fdd45c Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 20:31:34 +0200 Subject: add unique indexes to gpg_keys --- db/migrate/20170222111732_create_gpg_keys.rb | 3 ++- db/schema.rb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/db/migrate/20170222111732_create_gpg_keys.rb b/db/migrate/20170222111732_create_gpg_keys.rb index ec619394f6a..541228e8735 100644 --- a/db/migrate/20170222111732_create_gpg_keys.rb +++ b/db/migrate/20170222111732_create_gpg_keys.rb @@ -12,7 +12,8 @@ class CreateGpgKeys < ActiveRecord::Migration t.text :key - t.index :primary_keyid, length: Gitlab::Database.mysql? ? 20 : nil + 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 diff --git a/db/schema.rb b/db/schema.rb index 1a7eb2ded76..2cc8b1624c0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -549,7 +549,8 @@ ActiveRecord::Schema.define(version: 20170725145659) do t.text "key" end - add_index "gpg_keys", ["primary_keyid"], name: "index_gpg_keys_on_primary_keyid", using: :btree + 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| -- cgit v1.2.1 From 843b1de0dec3e101b323737e4d345c4e58b2a0c3 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 20:35:44 +0200 Subject: simplify nil handling --- app/models/gpg_key.rb | 3 +-- spec/models/gpg_key_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 0d35baa7ade..009a93ce1a8 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -46,8 +46,7 @@ class GpgKey < ActiveRecord::Base end def key=(value) - value.strip! unless value.blank? - write_attribute(:key, value) + super(value&.strip) end def user_infos diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 06bdbb59a11..1242f0b2e2a 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -44,6 +44,10 @@ describe GpgKey do 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 -- cgit v1.2.1 From a5f04df8d76d7c3c4318820fc3053a9823143dba Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 21:14:14 +0200 Subject: update all records at once using `update_all` --- app/models/gpg_key.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 009a93ce1a8..535b40472b0 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -77,12 +77,11 @@ class GpgKey < ActiveRecord::Base end def revoke - GpgSignature.where(gpg_key: self, valid_signature: true).find_each do |gpg_signature| - gpg_signature.update_attributes!( - gpg_key: nil, - valid_signature: false - ) - end + GpgSignature.where(gpg_key: self, valid_signature: true).update_all( + gpg_key_id: nil, + valid_signature: false, + updated_at: Time.zone.now + ) destroy end -- cgit v1.2.1 From fef030c23dff6f3b11b0e6bfd4c9443106375de1 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Tue, 25 Jul 2017 21:20:48 +0200 Subject: validate the foreign_key instead of the relation --- app/models/gpg_signature.rb | 2 +- spec/models/gpg_signature_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index cb69106183d..1ac0e123ff1 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -8,7 +8,7 @@ class GpgSignature < ActiveRecord::Base belongs_to :gpg_key validates :commit_sha, presence: true - validates :project, presence: true + validates :project_id, presence: true validates :gpg_key_primary_keyid, presence: true def gpg_key_primary_keyid diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb index b6f256e61ee..9a9b1900aa5 100644 --- a/spec/models/gpg_signature_spec.rb +++ b/spec/models/gpg_signature_spec.rb @@ -9,7 +9,7 @@ RSpec.describe GpgSignature do describe 'validation' do subject { described_class.new } it { is_expected.to validate_presence_of(:commit_sha) } - it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:project_id) } it { is_expected.to validate_presence_of(:gpg_key_primary_keyid) } end -- cgit v1.2.1 From 4e53131f7dceb001368446ef3e7eb3747cfcec02 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 26 Jul 2017 09:30:33 +0200 Subject: add unique index for gpg_signatures#commit_sha --- db/migrate/20170613154149_create_gpg_signatures.rb | 2 +- db/schema.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20170613154149_create_gpg_signatures.rb b/db/migrate/20170613154149_create_gpg_signatures.rb index 775a9463914..f6b5e7ebb7b 100644 --- a/db/migrate/20170613154149_create_gpg_signatures.rb +++ b/db/migrate/20170613154149_create_gpg_signatures.rb @@ -16,7 +16,7 @@ class CreateGpgSignatures < ActiveRecord::Migration t.text :gpg_key_user_name t.text :gpg_key_user_email - t.index :commit_sha, length: Gitlab::Database.mysql? ? 20 : nil + 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 diff --git a/db/schema.rb b/db/schema.rb index 2cc8b1624c0..63030350c5d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -565,7 +565,7 @@ ActiveRecord::Schema.define(version: 20170725145659) do t.text "gpg_key_user_email" end - add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", using: :btree + 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 -- cgit v1.2.1 From 9488b7780edc57193cd1c51888478538ddc94e51 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 26 Jul 2017 10:24:46 +0200 Subject: optimize query, only select relevant db columns --- lib/gitlab/gpg/invalid_gpg_signature_updater.rb | 1 + .../gpg/invalid_gpg_signature_updater_spec.rb | 64 ++++++++++++++++++---- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index 1782e20dcab..3bb491120ba 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -7,6 +7,7 @@ module Gitlab 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| diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index 5a81a86b93c..c4e04ee46a2 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do end before do - allow_any_instance_of(GpgSignature).to receive(:commit).and_return(commit) + 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 @@ -41,7 +41,13 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do key: GpgHelpers::User1.public_key, user: user - expect(valid_gpg_signature.reload.gpg_key).to eq gpg_key + 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 @@ -50,7 +56,13 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do key: GpgHelpers::User2.public_key, user: user - expect(valid_gpg_signature.reload.gpg_key).to be_nil + 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 @@ -68,11 +80,17 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do it 'updates the signature to being valid when the missing gpg key is added' do # InvalidGpgSignatureUpdater is called by the after_create hook - create :gpg_key, + gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: user - expect(invalid_gpg_signature.reload.valid_signature).to be_truthy + 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 @@ -81,7 +99,13 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do key: GpgHelpers::User2.public_key, user: user - expect(invalid_gpg_signature.reload.valid_signature).to be_falsey + 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 @@ -102,7 +126,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do end it 'updates the signature to being valid when the user updates the email address' do - create :gpg_key, + gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: user @@ -111,20 +135,38 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do # InvalidGpgSignatureUpdater is called by the after_update hook user.update_attributes!(email: GpgHelpers::User1.emails.first) - expect(invalid_gpg_signature.reload.valid_signature).to be_truthy + 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 - create :gpg_key, + gpg_key = create :gpg_key, key: GpgHelpers::User1.public_key, user: user - expect(invalid_gpg_signature.reload.valid_signature).to be_falsey + 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.valid_signature).to be_falsey + 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 -- cgit v1.2.1 From f1ccecc9979e3091e7cf54f98508f6bc7c01a7f5 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 26 Jul 2017 15:47:00 +0200 Subject: improve gpg key validation when omitting the end part of the key ('-----END PGP PUBLIC KEY BLOCK-----') the error message was not about the key anymore, but about the missing fingerprint and primary_keyid, which was confusing for the user. the new validation checks that the end also matches the expected format. --- app/models/gpg_key.rb | 5 +++-- spec/models/gpg_key_spec.rb | 10 +++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 535b40472b0..3df60ddc950 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -1,5 +1,6 @@ class GpgKey < ActiveRecord::Base KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze + KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----'.freeze include ShaAttribute @@ -15,8 +16,8 @@ class GpgKey < ActiveRecord::Base presence: true, uniqueness: true, format: { - with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX}).)+\Z/m, - message: "is invalid. A valid public GPG key begins with '#{KEY_PREFIX}'" + 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, diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 1242f0b2e2a..59c074199db 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -7,10 +7,18 @@ describe GpgKey do 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").for(: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 -- cgit v1.2.1 From 7f7e93a34471f673ac3888549c67bce4e763300e Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 26 Jul 2017 16:01:24 +0200 Subject: remove log statements from workers --- app/workers/create_gpg_signature_worker.rb | 8 ++------ app/workers/invalid_gpg_signature_update_worker.rb | 4 +--- spec/workers/create_gpg_signature_worker_spec.rb | 14 -------------- spec/workers/invalid_gpg_signature_update_worker_spec.rb | 7 ------- 4 files changed, 3 insertions(+), 30 deletions(-) diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index 6fbd6e1a3f3..4f47717ff69 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -5,15 +5,11 @@ class CreateGpgSignatureWorker def perform(commit_sha, project_id) project = Project.find_by(id: project_id) - unless project - return Rails.logger.error("CreateGpgSignatureWorker: couldn't find project with ID=#{project_id}, skipping job") - end + return unless project commit = project.commit(commit_sha) - unless commit - return Rails.logger.error("CreateGpgSignatureWorker: couldn't find commit with commit_sha=#{commit_sha}, skipping job") - end + return unless commit commit.signature end diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb index c0bec3c9689..db6b1ea8e8d 100644 --- a/app/workers/invalid_gpg_signature_update_worker.rb +++ b/app/workers/invalid_gpg_signature_update_worker.rb @@ -5,9 +5,7 @@ class InvalidGpgSignatureUpdateWorker def perform(gpg_key_id) gpg_key = GpgKey.find_by(id: gpg_key_id) - unless gpg_key - return Rails.logger.error("InvalidGpgSignatureUpdateWorker: couldn't find gpg_key with ID=#{gpg_key_id}, skipping job") - end + return unless gpg_key Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run end diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb index a23f0d6c34a..c6a17d77d73 100644 --- a/spec/workers/create_gpg_signature_worker_spec.rb +++ b/spec/workers/create_gpg_signature_worker_spec.rb @@ -20,13 +20,6 @@ describe CreateGpgSignatureWorker do let(:nonexisting_commit_sha) { 'bogus' } let(:project) { create :project } - it 'logs CreateGpgSignatureWorker process skipping' do - expect(Rails.logger).to receive(:error) - .with("CreateGpgSignatureWorker: couldn't find commit with commit_sha=bogus, skipping job") - - described_class.new.perform(nonexisting_commit_sha, project.id) - end - it 'does not raise errors' do expect { described_class.new.perform(nonexisting_commit_sha, project.id) }.not_to raise_error end @@ -41,13 +34,6 @@ describe CreateGpgSignatureWorker do context 'when Project is not found' do let(:nonexisting_project_id) { -1 } - it 'logs CreateGpgSignatureWorker process skipping' do - expect(Rails.logger).to receive(:error) - .with("CreateGpgSignatureWorker: couldn't find project with ID=-1, skipping job") - - described_class.new.perform(anything, nonexisting_project_id) - end - it 'does not raise errors' do expect { described_class.new.perform(anything, nonexisting_project_id) }.not_to raise_error end diff --git a/spec/workers/invalid_gpg_signature_update_worker_spec.rb b/spec/workers/invalid_gpg_signature_update_worker_spec.rb index 8d568076e1a..5972696515b 100644 --- a/spec/workers/invalid_gpg_signature_update_worker_spec.rb +++ b/spec/workers/invalid_gpg_signature_update_worker_spec.rb @@ -16,13 +16,6 @@ describe InvalidGpgSignatureUpdateWorker do context 'when GpgKey is not found' do let(:nonexisting_gpg_key_id) { -1 } - it 'logs InvalidGpgSignatureUpdateWorker process skipping' do - expect(Rails.logger).to receive(:error) - .with("InvalidGpgSignatureUpdateWorker: couldn't find gpg_key with ID=-1, skipping job") - - described_class.new.perform(nonexisting_gpg_key_id) - end - it 'does not raise errors' do expect { described_class.new.perform(nonexisting_gpg_key_id) }.not_to raise_error end -- cgit v1.2.1 From 5ebccab1eb74f7bf9f7f9d4f2d9a56fb81754cbe Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 27 Jul 2017 16:01:24 +0200 Subject: add "GPG Keys" to new navigation --- app/views/layouts/nav/_new_profile_sidebar.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml index 239e6b949e2..6bbd569583e 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -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 -- cgit v1.2.1