]> review.fuel-infra Code Review - puppet-modules/puppetlabs-apt.git/blob - lib/puppet/provider/apt_key/apt_key.rb
(maint) Remove uneeded workarounds for ruby/facter
[puppet-modules/puppetlabs-apt.git] / lib / puppet / provider / apt_key / apt_key.rb
1 require 'open-uri'
2 require 'net/ftp'
3 require 'tempfile'
4
5 Puppet::Type.type(:apt_key).provide(:apt_key) do
6   desc 'apt-key provider for apt_key resource'
7
8   confine    osfamily: :debian
9   defaultfor osfamily: :debian
10   commands   apt_key: 'apt-key'
11   commands   gpg: '/usr/bin/gpg'
12
13   def self.instances
14     cli_args = ['adv', '--no-tty', '--list-keys', '--with-colons', '--fingerprint', '--fixed-list-mode']
15
16     key_output = apt_key(cli_args).encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
17
18     pub_line, sub_line, fpr_line = nil
19
20     key_array = key_output.split("\n").map do |line|
21       if line.start_with?('pub')
22         pub_line = line
23         # reset fpr_line, to skip any previous subkeys which were collected
24         fpr_line = nil
25         sub_line = nil
26       elsif line.start_with?('sub')
27         sub_line = line
28       elsif line.start_with?('fpr')
29         fpr_line = line
30       end
31
32       if sub_line && fpr_line
33         sub_line, fpr_line = nil
34         next
35       end
36
37       next unless pub_line && fpr_line
38
39       line_hash = key_line_hash(pub_line, fpr_line)
40
41       # reset everything
42       pub_line, fpr_line = nil
43
44       expired = false
45
46       if line_hash[:key_expiry]
47         expired = Time.now >= line_hash[:key_expiry]
48       end
49
50       new(
51         name: line_hash[:key_fingerprint],
52         id: line_hash[:key_long],
53         fingerprint: line_hash[:key_fingerprint],
54         short: line_hash[:key_short],
55         long: line_hash[:key_long],
56         ensure: :present,
57         expired: expired,
58         expiry: line_hash[:key_expiry].nil? ? nil : line_hash[:key_expiry].strftime('%Y-%m-%d'),
59         size: line_hash[:key_size],
60         type: line_hash[:key_type],
61         created: line_hash[:key_created].strftime('%Y-%m-%d'),
62       )
63     end
64     key_array.compact!
65   end
66
67   def self.prefetch(resources)
68     apt_keys = instances
69     resources.each_key do |name|
70       if name.length == 40
71         provider = apt_keys.find { |key| key.fingerprint == name }
72         resources[name].provider = provider if provider
73       elsif name.length == 16
74         provider = apt_keys.find { |key| key.long == name }
75         resources[name].provider = provider if provider
76       elsif name.length == 8
77         provider = apt_keys.find { |key| key.short == name }
78         resources[name].provider = provider if provider
79       end
80     end
81   end
82
83   def self.key_line_hash(pub_line, fpr_line)
84     pub_split = pub_line.split(':')
85     fpr_split = fpr_line.split(':')
86
87     fingerprint = fpr_split.last
88     return_hash = {
89       key_fingerprint: fingerprint,
90       key_long: fingerprint[-16..-1], # last 16 characters of fingerprint
91       key_short: fingerprint[-8..-1], # last 8 characters of fingerprint
92       key_size: pub_split[2],
93       key_type: nil,
94       key_created: Time.at(pub_split[5].to_i),
95       key_expiry: pub_split[6].empty? ? nil : Time.at(pub_split[6].to_i),
96     }
97
98     # set key type based on types defined in /usr/share/doc/gnupg/DETAILS.gz
99     case pub_split[3]
100     when '1'
101       return_hash[:key_type] = :rsa
102     when '17'
103       return_hash[:key_type] = :dsa
104     when '18'
105       return_hash[:key_type] = :ecc
106     when '19'
107       return_hash[:key_type] = :ecdsa
108     end
109
110     return_hash
111   end
112
113   def source_to_file(value)
114     parsed_value = URI.parse(value)
115     if parsed_value.scheme.nil?
116       raise(_('The file %{_value} does not exist') % { _value: value }) unless File.exist?(value)
117       # Because the tempfile method has to return a live object to prevent GC
118       # of the underlying file from occuring too early, we also have to return
119       # a file object here.  The caller can still call the #path method on the
120       # closed file handle to get the path.
121       f = File.open(value, 'r')
122       f.close
123       f
124     else
125       begin
126         # Only send basic auth if URL contains userinfo
127         # Some webservers (e.g. Amazon S3) return code 400 if empty basic auth is sent
128         if parsed_value.userinfo.nil?
129           key = parsed_value.read
130         else
131           user_pass = parsed_value.userinfo.split(':')
132           parsed_value.userinfo = ''
133           key = open(parsed_value, http_basic_authentication: user_pass).read
134         end
135       rescue OpenURI::HTTPError, Net::FTPPermError => e
136         raise(_('%{_e} for %{_resource}') % { _e: e.message, _resource: resource[:source] })
137       rescue SocketError
138         raise(_('could not resolve %{_resource}') % { _resource: resource[:source] })
139       else
140         tempfile(key)
141       end
142     end
143   end
144
145   # The tempfile method needs to return the tempfile object to the caller, so
146   # that it doesn't get deleted by the GC immediately after it returns.  We
147   # want the caller to control when it goes out of scope.
148   def tempfile(content)
149     file = Tempfile.new('apt_key')
150     file.write content
151     file.close
152     # confirm that the fingerprint from the file, matches the long key that is in the manifest
153     if name.size == 40
154       if File.executable? command(:gpg)
155         extracted_key = execute(["#{command(:gpg)} --no-tty --with-fingerprint --with-colons #{file.path} | awk -F: '/^fpr:/ { print $10 }'"], failonfail: false)
156         extracted_key = extracted_key.chomp
157
158         found_match = false
159         extracted_key.each_line do |line|
160           if line.chomp == name
161             found_match = true
162           end
163         end
164         unless found_match
165           raise(_('The id in your manifest %{_resource} and the fingerprint from content/source don\'t match. Check for an error in the id and content/source is legitimate.') % { _resource: resource[:name] }) # rubocop:disable Metrics/LineLength
166         end
167       else
168         warning('/usr/bin/gpg cannot be found for verification of the id.')
169       end
170     end
171     file
172   end
173
174   # Update a key if it is expired
175   def update_expired_key
176     # Return without doing anything if refresh or expired is false
177     return unless resource[:refresh] == true && resource[:expired] == true
178
179     # Execute command to update key
180     command = []
181
182     unless resource[:source].nil? && resource[:content].nil?
183       raise(_('an unexpected condition occurred while trying to add the key: %{_resource}') % { _resource: resource[:id] })
184     end
185
186     # Breaking up the command like this is needed because it blows up
187     # if --recv-keys isn't the last argument.
188     command.push('adv', '--no-tty', '--keyserver', resource[:server])
189     unless resource[:options].nil?
190       command.push('--keyserver-options', resource[:options])
191     end
192     command.push('--recv-keys', resource[:id])
193   end
194
195   def exists?
196     update_expired_key
197     # report expired keys as non-existing when refresh => true
198     @property_hash[:ensure] == :present && !(resource[:refresh] && @property_hash[:expired])
199   end
200
201   def create
202     command = []
203     if resource[:source].nil? && resource[:content].nil?
204       # Breaking up the command like this is needed because it blows up
205       # if --recv-keys isn't the last argument.
206       command.push('adv', '--no-tty', '--keyserver', resource[:server])
207       unless resource[:options].nil?
208         command.push('--keyserver-options', resource[:options])
209       end
210       command.push('--recv-keys', resource[:id])
211     elsif resource[:content]
212       key_file = tempfile(resource[:content])
213       command.push('add', key_file.path)
214     elsif resource[:source]
215       key_file = source_to_file(resource[:source])
216       command.push('add', key_file.path)
217     # In case we really screwed up, better safe than sorry.
218     else
219       raise(_('an unexpected condition occurred while trying to add the key: %{_resource}') % { _resource: resource[:id] })
220     end
221     apt_key(command)
222     @property_hash[:ensure] = :present
223   end
224
225   def destroy
226     loop do
227       apt_key('del', resource.provider.short)
228       r = execute(["#{command(:apt_key)} list | grep '/#{resource.provider.short}\s'"], failonfail: false)
229       break unless r.exitstatus.zero?
230     end
231     @property_hash.clear
232   end
233
234   def read_only(_value)
235     raise(_('This is a read-only property.'))
236   end
237
238   mk_resource_methods
239
240   # Alias the setters of read-only properties
241   # to the read_only function.
242   alias_method :created=, :read_only
243   alias_method :expired=, :read_only
244   alias_method :expiry=, :read_only
245   alias_method :size=, :read_only
246   alias_method :type=, :read_only
247 end