1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
# frozen_string_literal: true
module Gitlab
module Gpg
extend self
CleanupError = Class.new(StandardError)
BG_CLEANUP_RUNTIME_S = 10
FG_CLEANUP_RUNTIME_S = 0.5
MUTEX = Mutex.new
module CurrentKeyChain
extend self
def add(key)
GPGME::Key.import(key)
end
def fingerprints_from_key(key)
import = GPGME::Key.import(key)
return [] if import.imported == 0
import.imports.map(&:fingerprint)
end
end
def fingerprints_from_key(key)
using_tmp_keychain do
CurrentKeyChain.fingerprints_from_key(key)
end
end
def primary_keyids_from_key(key)
using_tmp_keychain do
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
GPGME::Key.find(:public, fingerprints).map { |raw_key| raw_key.primary_subkey.keyid }
end
end
def subkeys_from_key(key)
using_tmp_keychain do
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
raw_keys = GPGME::Key.find(:public, fingerprints)
raw_keys.each_with_object({}) do |raw_key, grouped_subkeys|
primary_subkey_id = raw_key.primary_subkey.keyid
grouped_subkeys[primary_subkey_id] = raw_key.subkeys[1..-1].map do |s|
{ keyid: s.keyid, fingerprint: s.fingerprint }
end
end
end
end
def user_infos_from_key(key)
using_tmp_keychain do
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
GPGME::Key.find(:public, fingerprints).flat_map do |raw_key|
raw_key.uids.each_with_object([]) do |uid, arr|
name = uid.name.force_encoding('UTF-8')
email = uid.email.force_encoding('UTF-8')
arr << { name: name, email: email.downcase } if name.valid_encoding? && email.valid_encoding?
end
end
end
end
# Allows thread safe switching of temporary keychain files
#
# 1. The current thread may use nesting of temporary keychain
# 2. Another thread needs to wait for the lock to be released
def using_tmp_keychain(&block)
if MUTEX.locked? && MUTEX.owned?
optimistic_using_tmp_keychain(&block)
else
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
MUTEX.synchronize do
optimistic_using_tmp_keychain(&block)
end
end
end
end
# 1. Returns the custom home directory if one has been set by calling
# `GPGME::Engine.home_dir=`
# 2. Returns the default home directory otherwise
def current_home_dir
GPGME::Engine.info.first.home_dir || GPGME::Engine.dirinfo('homedir')
end
private
def optimistic_using_tmp_keychain
previous_dir = current_home_dir
tmp_dir = Dir.mktmpdir
GPGME::Engine.home_dir = tmp_dir
tmp_keychains_created.increment
yield
ensure
GPGME::Engine.home_dir = previous_dir
begin
cleanup_tmp_dir(tmp_dir)
rescue CleanupError => e
folder_contents = Dir.children(tmp_dir)
# This means we left a GPG-agent process hanging. Logging the problem in
# sentry will make this more visible.
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e,
issue_url: 'https://gitlab.com/gitlab-org/gitlab/issues/20918',
tmp_dir: tmp_dir, contents: folder_contents)
end
tmp_keychains_removed.increment unless File.exist?(tmp_dir)
end
def cleanup_tmp_dir(tmp_dir)
# Retry when removing the tmp directory failed, as we may run into a
# race condition:
# The `gpg-agent` agent process may clean up some files as well while
# `FileUtils.remove_entry` is iterating the directory and removing all
# its contained files and directories recursively, which could raise an
# error.
# Failing to remove the tmp directory could leave the `gpg-agent` process
# running forever.
Retriable.retriable(max_elapsed_time: cleanup_time, base_interval: 0.1) do
FileUtils.remove_entry(tmp_dir) if File.exist?(tmp_dir)
end
rescue => e
raise CleanupError, e
end
def cleanup_time
Sidekiq.server? ? BG_CLEANUP_RUNTIME_S : FG_CLEANUP_RUNTIME_S
end
def tmp_keychains_created
@tmp_keychains_created ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total,
'The number of temporary GPG keychains created')
end
def tmp_keychains_removed
@tmp_keychains_removed ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total,
'The number of temporary GPG keychains removed')
end
end
end
|