puppetserver-ca 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 020665126fef9a8fe7bc63652247bf13cf909ab37e2a3336d43724bc63b26d32
4
- data.tar.gz: c8d62416aaa1c4ded40daee19907798a13ed6fa3ea123436701adf88b9b497cd
3
+ metadata.gz: f3405b0e0be4f947fcec85824880f6b62fb2630d1993e130e1782960e58b5a80
4
+ data.tar.gz: 92ae8e4b074c7d12609a79beb4c886a95f3299e44b2b4ff602f8b6acf7b002c2
5
5
  SHA512:
6
- metadata.gz: 89f506f4124c95b8b7029043ce46f216b837d3863875077476bfb835cbaf83399734d02dc7bd64ec39234d4d68735e98997ea81a724146ccef7b540e02d59849
7
- data.tar.gz: 33b9fa7893650441e456e39edda8d5d4532bcd52fe42f0832769eafad982655d2eb26357b51080a90d1da313b4a69d02f01119ae9dc1e4af645641664e9f8699
6
+ metadata.gz: 82fce6d5d561700df09dc132723a600f2943ca86d19b7a2cbe56b1c25c4ebce170cb4a973ef6a07d0271057587cccbd2185c1a392dba9217b6dd6a8c617ce3df
7
+ data.tar.gz: 130b8dc609b09c2ab7722459f62febcce52452ea419fa69f4ecb70d059b28428962574206a6bc5600f620f541c4205f86fe9e2eb7381a5bee6bb2e9fa24941b4
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
data/.travis.yml CHANGED
@@ -8,6 +8,8 @@ rvm:
8
8
  - 2.5
9
9
  - 2.6
10
10
  - 2.7
11
+ - 3.1
12
+ - 3.2
11
13
  before_install:
12
14
  gem install bundler -v 1.16.1 && (gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true)
13
15
  script:
data/CODEOWNERS CHANGED
@@ -1,4 +1,4 @@
1
1
  # This will cause the puppetserver-maintainers group to be assigned
2
2
  # review of any opened PRs against the branches containing this file.
3
3
 
4
- * @puppetlabs/puppetserver-maintainers
4
+ * @puppetlabs/dumpling @puppetlabs/skeletor
data/README.md CHANGED
@@ -101,16 +101,11 @@ To test your changes on a VM:
101
101
  ```
102
102
  1. To confirm that installation was successful, run `puppetserver ca --help`
103
103
 
104
- ### Releasing
105
- To release a new version, run the [release pipeline](https://jenkins-platform.delivery.puppetlabs.net/job/platform_puppetserver-ca_init-multijob_main/), which will bump the version, tag, build, and release the gem.
106
-
107
104
  ## Contributing & Support
108
105
 
109
- Bug reports and feature requests are welcome in JIRA at
110
- https://tickets.puppetlabs.com/projects/SERVER/issues.
106
+ Bug reports and feature requests are welcome via GitHub issues.
111
107
 
112
- For interactive questions feel free to post to #puppet or #puppet-dev on
113
- Freenode, or the Puppet Community Slack channel.
108
+ For interactive questions feel free to post to #puppet or #puppet-dev on the Puppet Community Slack channel.
114
109
 
115
110
  Contributions are welcome at https://github.com/puppetlabs/puppetserver-ca-cli/pulls.
116
111
  Contributors should both be sure to read the
@@ -19,7 +19,7 @@ module Puppetserver
19
19
  Usage:
20
20
  puppetserver ca setup [--help]
21
21
  puppetserver ca setup [--config PATH] [--subject-alt-names NAME[,NAME]]
22
- [--certname NAME] [--ca-name NAME]
22
+ [--certname NAME] [--ca-name NAME] [--root-ca-name NAME]
23
23
 
24
24
  Description:
25
25
  Setup a root and intermediate signing CA for Puppet Server
@@ -51,6 +51,7 @@ BANNER
51
51
  settings_overrides = {}
52
52
  settings_overrides[:certname] = input['certname'] unless input['certname'].empty?
53
53
  settings_overrides[:ca_name] = input['ca-name'] unless input['ca-name'].empty?
54
+ settings_overrides[:root_ca_name] = input['root-ca-name'] unless input['root-ca-name'].empty?
54
55
  # Since puppet expects the key to be called 'dns_alt_names', we need to use that here
55
56
  # to ensure that the overriding works correctly.
56
57
  settings_overrides[:dns_alt_names] = input['subject-alt-names'] unless input['subject-alt-names'].empty?
@@ -153,6 +154,7 @@ ERR
153
154
  def self.parser(parsed = {})
154
155
  parsed['subject-alt-names'] = ''
155
156
  parsed['ca-name'] = ''
157
+ parsed['root-ca-name'] = ''
156
158
  parsed['certname'] = ''
157
159
  OptionParser.new do |opts|
158
160
  opts.banner = BANNER
@@ -170,6 +172,10 @@ ERR
170
172
  'Common name to use for the CA signing cert') do |name|
171
173
  parsed['ca-name'] = name
172
174
  end
175
+ opts.on('--root-ca-name NAME',
176
+ 'Common name to use for the self-signed Root CA cert') do |name|
177
+ parsed['root-ca-name'] = name
178
+ end
173
179
  opts.on('--certname NAME',
174
180
  'Common name to use for the server cert') do |name|
175
181
  parsed['certname'] = name
@@ -66,17 +66,33 @@ Options:
66
66
  return 1 if Errors.handle_with_usage(@logger, puppet.errors)
67
67
 
68
68
  ca = Puppetserver::Ca::CertificateAuthority.new(@logger, puppet.settings)
69
+ bulk_sign = ca.server_has_bulk_signing_endpoints
70
+
71
+ # Bulk sign endpoints don't allow setting TTL, so
72
+ # use single signing endpoint if TTL is specified.
73
+ success = false
74
+ if input['ttl'] || !bulk_sign
75
+ if input['all']
76
+ requested_certnames = get_all_pending_certs(ca)
77
+ return 1 if requested_certnames.nil?
78
+ return 24 if requested_certnames.empty?
79
+ else
80
+ requested_certnames = input['certname']
81
+ end
69
82
 
70
- if input['all']
71
- requested_certnames = get_all_pending_certs(ca)
72
- return 1 if requested_certnames.nil?
73
- return 24 if requested_certnames.empty?
83
+ success = ca.sign_certs(requested_certnames, input['ttl'])
84
+ return success ? 0 : 1
74
85
  else
75
- requested_certnames = input['certname']
86
+ result = input['all'] ? ca.sign_all : ca.sign_bulk(input['certname'])
87
+ case result
88
+ when :success
89
+ return 0
90
+ when :no_requests
91
+ return 24
92
+ else
93
+ return 1
94
+ end
76
95
  end
77
-
78
- success = ca.sign_certs(requested_certnames, input['ttl'])
79
- return success ? 0 : 1
80
96
  end
81
97
 
82
98
  def get_all_pending_certs(ca)
@@ -19,7 +19,6 @@ module Puppetserver
19
19
  }
20
20
 
21
21
  REVOKE_BODY = JSON.dump({ desired_state: 'revoked' })
22
- SIGN_BODY = JSON.dump({ desired_state: 'signed' })
23
22
 
24
23
  def initialize(logger, settings)
25
24
  @logger = logger
@@ -28,6 +27,15 @@ module Puppetserver
28
27
  @ca_port = settings[:ca_port]
29
28
  end
30
29
 
30
+ def server_has_bulk_signing_endpoints
31
+ url = HttpClient::URL.new('https', @ca_server, @ca_port, 'status', 'v1', 'services')
32
+ result = @client.with_connection(url) do |connection|
33
+ connection.get(url)
34
+ end
35
+ version = process_results(:server_version, nil, result)
36
+ return version >= Gem::Version.new('8.4.0')
37
+ end
38
+
31
39
  def worst_result(previous_result, current_result)
32
40
  %i{success invalid not_found error}.each do |state|
33
41
  if previous_result == state
@@ -61,13 +69,26 @@ module Puppetserver
61
69
  end
62
70
  end
63
71
 
72
+ def sign_all
73
+ return post(resource_type: 'sign',
74
+ resource_name: 'all',
75
+ body: '{}',
76
+ type: :sign_all)
77
+ end
78
+
79
+ def sign_bulk(certnames)
80
+ return post(resource_type: 'sign',
81
+ body: "{\"certnames\":#{certnames}}",
82
+ type: :sign_bulk
83
+ )
84
+ end
85
+
64
86
  def sign_certs(certnames, ttl=nil)
65
87
  results = []
66
88
  if ttl
67
89
  lifetime = process_ttl_input(ttl)
68
90
  return false if lifetime.nil?
69
- body = JSON.dump({ desired_state: 'signed',
70
- cert_ttl: lifetime})
91
+ body = JSON.dump({ desired_state: 'signed', cert_ttl: lifetime})
71
92
  results = put(certnames,
72
93
  resource_type: 'certificate_status',
73
94
  body: body,
@@ -75,7 +96,7 @@ module Puppetserver
75
96
  else
76
97
  results = put(certnames,
77
98
  resource_type: 'certificate_status',
78
- body: SIGN_BODY,
99
+ body: JSON.dump({ desired_state: 'signed' }),
79
100
  type: :sign)
80
101
  end
81
102
 
@@ -119,6 +140,48 @@ module Puppetserver
119
140
  end
120
141
  end
121
142
 
143
+ # Make an HTTP POST request to CA
144
+ # @param endpoint [String] the endpoint to post to for the url
145
+ # @param body [JSON/String] body of the post request
146
+ # @param type [Symbol] type of error processing to perform on result
147
+ # @return [Boolean] whether all requests were successful
148
+ def post(resource_type:, resource_name: nil, body:, type:, headers: {})
149
+ url = make_ca_url(resource_type, resource_name)
150
+ results = @client.with_connection(url) do |connection|
151
+ result = connection.post(body, url, headers)
152
+ process_results(type, nil, result)
153
+ end
154
+ end
155
+
156
+ # Handle the result data from the /sign and /sign/all endpoints
157
+ def process_bulk_sign_result_data(result)
158
+ data = JSON.parse(result.body)
159
+ signed = data.dig('signed') || []
160
+ no_csr = data.dig('no-csr') || []
161
+ signing_errors = data.dig('signing-errors') || []
162
+
163
+ if !signed.empty?
164
+ @logger.inform "Successfully signed the following certificate requests:"
165
+ signed.each { |s| @logger.inform " #{s}" }
166
+ end
167
+
168
+ @logger.err 'Error:' if !no_csr.empty? || !signing_errors.empty?
169
+ if !no_csr.empty?
170
+ @logger.err ' No certificate request found for the following nodes when attempting to sign:'
171
+ no_csr.each { |s| @logger.err " #{s}" }
172
+ end
173
+ if !signing_errors.empty?
174
+ @logger.err ' Error encountered when attempting to sign the certificate request for the following nodes:'
175
+ signing_errors.each { |s| @logger.err " #{s}" }
176
+ end
177
+ if no_csr.empty? && signing_errors.empty?
178
+ @logger.err 'No waiting certificate requests to sign.' if signed.empty?
179
+ return signed.empty? ? :no_requests : :success
180
+ else
181
+ return :error
182
+ end
183
+ end
184
+
122
185
  # logs the action and returns true/false for success
123
186
  def process_results(type, certname, result)
124
187
  case type
@@ -138,6 +201,50 @@ module Puppetserver
138
201
  @logger.err " body: #{result.body.to_s}" if result.body
139
202
  return :error
140
203
  end
204
+ when :sign_all
205
+ if result.code == '200'
206
+ if !result.body
207
+ @logger.err 'Error:'
208
+ @logger.err ' Response from /sign/all endpoint did not include a body. Unable to verify certificate requests were signed.'
209
+ return :error
210
+ end
211
+ begin
212
+ return process_bulk_sign_result_data(result)
213
+ rescue JSON::ParserError
214
+ @logger.err 'Error:'
215
+ @logger.err ' Unable to parse the response from the /sign/all endpoint.'
216
+ @logger.err " body #{result.body.to_s}"
217
+ return :error
218
+ end
219
+ else
220
+ @logger.err 'Error:'
221
+ @logger.err ' When attempting to sign all certificate requests, received:'
222
+ @logger.err " code: #{result.code}"
223
+ @logger.err " body: #{result.body.to_s}" if result.body
224
+ return :error
225
+ end
226
+ when :sign_bulk
227
+ if result.code == '200'
228
+ if !result.body
229
+ @logger.err 'Error:'
230
+ @logger.err ' Response from /sign endpoint did not include a body. Unable to verify certificate requests were signed.'
231
+ return :error
232
+ end
233
+ begin
234
+ return process_bulk_sign_result_data(result)
235
+ rescue JSON::ParserError
236
+ @logger.err 'Error:'
237
+ @logger.err ' Unable to parse the response from the /sign endpoint.'
238
+ @logger.err " body #{result.body.to_s}"
239
+ return :error
240
+ end
241
+ else
242
+ @logger.err 'Error:'
243
+ @logger.err ' When attempting to sign certificate requests, received:'
244
+ @logger.err " code: #{result.code}"
245
+ @logger.err " body: #{result.body.to_s}" if result.body
246
+ return :error
247
+ end
141
248
  when :revoke
142
249
  case result.code
143
250
  when '200', '204'
@@ -170,6 +277,19 @@ module Puppetserver
170
277
  @logger.err " body: #{result.body.to_s}" if result.body
171
278
  return :error
172
279
  end
280
+ when :server_version
281
+ if result.code == '200' && result.body
282
+ begin
283
+ data = JSON.parse(result.body)
284
+ version_str = data.dig('ca','service_version')
285
+ return Gem::Version.new(version_str.match('^\d+\.\d+\.\d+')[0])
286
+ rescue JSON::ParserError, NoMethodError
287
+ # If we get bad JSON, version_str is nil, or the matcher doesn't match,
288
+ # fall through to returning a version of 0.
289
+ end
290
+ end
291
+ @logger.debug 'Could not detect server version. Defaulting to legacy signing endpoints.'
292
+ return Gem::Version.new(0)
173
293
  end
174
294
  end
175
295
 
@@ -194,6 +194,9 @@ module Puppetserver
194
194
  # class keys/section names but nearly any character values (excluding
195
195
  # leading whitespace) up to one of whitespace, opening curly brace, or
196
196
  # hash sign (Our concern being to capture filesystem path values).
197
+ #
198
+ # ca_root and root_ca_name values may include whitespace
199
+ #
197
200
  # Put values without a section into :main.
198
201
  #
199
202
  # Return Hash of Symbol section names with Symbol setting keys and
@@ -205,10 +208,15 @@ module Puppetserver
205
208
  case line
206
209
  when /^\s*\[(\w+)\].*/
207
210
  current_section = $1.to_sym
208
- when /^\s*(\w+)\s*=\s*([^\s{#]+).*$/
211
+ when /^\s*(\w+)\s*=\s*(.+?)\s*(?=[{#]|$)/
209
212
  # Using a Hash with a default key breaks RSpec expectations.
210
213
  res[current_section] ||= {}
211
- res[current_section][$1.to_sym] = $2
214
+ res[current_section][$1.to_sym] =
215
+ if [:ca_name, :root_ca_name].include?($1.to_sym)
216
+ $2
217
+ else
218
+ $2.split(' ')[0]
219
+ end
212
220
  end
213
221
  end
214
222
 
@@ -10,17 +10,12 @@ module Puppetserver
10
10
  # Utilities for doing HTTPS against the CA that wraps Net::HTTP constructs
11
11
  class HttpClient
12
12
 
13
- DEFAULT_HEADERS = {
14
- 'User-Agent' => 'PuppetserverCaCli',
15
- 'Content-Type' => 'application/json',
16
- 'Accept' => 'application/json'
17
- }
18
-
19
13
  attr_reader :store
20
14
 
21
15
  # Not all connections require a client cert to be present.
22
16
  # For example, when querying the status endpoint.
23
17
  def initialize(logger, settings, with_client_cert: true)
18
+ @default_headers = make_headers(ENV['HOME'])
24
19
  @logger = logger
25
20
  @store = make_store(settings[:localcacert],
26
21
  settings[:certificate_revocation],
@@ -52,7 +47,7 @@ module Puppetserver
52
47
  # The Connection object should have HTTP verbs defined on it that take
53
48
  # a body (and optional overrides). Returns whatever the block given returned.
54
49
  def with_connection(url, &block)
55
- request = ->(conn) { block.call(Connection.new(conn, url, @logger)) }
50
+ request = ->(conn) { block.call(Connection.new(conn, url, @logger, @default_headers)) }
56
51
 
57
52
  begin
58
53
  Net::HTTP.start(url.host, url.port,
@@ -68,7 +63,22 @@ module Puppetserver
68
63
 
69
64
  private
70
65
 
71
- def load_with_errors(path, setting, &block)
66
+ def make_headers(home)
67
+ headers = {
68
+ 'User-Agent' => 'PuppetserverCaCli',
69
+ 'Content-Type' => 'application/json',
70
+ 'Accept' => 'application/json'
71
+ }
72
+
73
+ token_path = "#{home}/.puppetlabs/token"
74
+ if File.exist?(token_path)
75
+ headers['X-Authentication'] = File.read(token_path)
76
+ end
77
+
78
+ headers
79
+ end
80
+
81
+ def load_with_errors(path, setting, &block)
72
82
  begin
73
83
  content = File.read(path)
74
84
  block.call(content)
@@ -81,21 +91,23 @@ module Puppetserver
81
91
  "Could not parse '#{setting}' at '#{path}'.\n" +
82
92
  " OpenSSL returned: #{e.message}")
83
93
  end
84
- end
94
+ end
85
95
 
86
96
  # Helper class that wraps a Net::HTTP connection, a HttpClient::URL
87
97
  # and defines methods named after HTTP verbs that are called on the
88
98
  # saved connection, returning a Result.
89
99
  class Connection
90
- def initialize(net_http_connection, url_struct, logger)
100
+
101
+ def initialize(net_http_connection, url_struct, logger, default_headers)
91
102
  @conn = net_http_connection
92
103
  @url = url_struct
93
104
  @logger = logger
105
+ @default_headers = default_headers
94
106
  end
95
107
 
96
- def get(url_overide = nil, headers = {})
108
+ def get(url_overide = nil, header_overrides = {})
97
109
  url = url_overide || @url
98
- headers = DEFAULT_HEADERS.merge(headers)
110
+ headers = @default_headers.merge(header_overrides)
99
111
 
100
112
  @logger.debug("Making a GET request at #{url.full_url}")
101
113
 
@@ -105,9 +117,9 @@ module Puppetserver
105
117
 
106
118
  end
107
119
 
108
- def put(body, url_override = nil, headers = {})
120
+ def put(body, url_override = nil, header_overrides = {})
109
121
  url = url_override || @url
110
- headers = DEFAULT_HEADERS.merge(headers)
122
+ headers = @default_headers.merge(header_overrides)
111
123
 
112
124
  @logger.debug("Making a PUT request at #{url.full_url}")
113
125
 
@@ -118,9 +130,22 @@ module Puppetserver
118
130
  Result.new(result.code, result.body)
119
131
  end
120
132
 
121
- def delete(url_override = nil, headers = {})
133
+ def post(body, url_override = nil, header_overrides = {})
134
+ url = url_override || @url
135
+ headers = @default_headers.merge(header_overrides)
136
+
137
+ @logger.debug("Making a POST request at #{url.full_url}")
138
+
139
+ request = Net::HTTP::Post.new(url.to_uri, headers)
140
+ request.body = body
141
+ result = @conn.request(request)
142
+
143
+ Result.new(result.code, result.body)
144
+ end
145
+
146
+ def delete(url_override = nil, header_overrides = {})
122
147
  url = url_override || @url
123
- headers = DEFAULT_HEADERS.merge(headers)
148
+ headers = @default_headers.merge(header_overrides)
124
149
 
125
150
  @logger.debug("Making a DELETE request at #{url.full_url}")
126
151
 
@@ -139,7 +164,7 @@ module Puppetserver
139
164
  :resource_type, :resource_name, :query) do
140
165
  def full_url
141
166
  url = protocol + '://' + host + ':' + port + '/' +
142
- [endpoint, version, resource_type, resource_name].join('/')
167
+ [endpoint, version, resource_type, resource_name].compact.join('/')
143
168
 
144
169
  url = url + "?" + URI.encode_www_form(query) unless query.nil? || query.empty?
145
170
  return url
@@ -1,5 +1,5 @@
1
1
  module Puppetserver
2
2
  module Ca
3
- VERSION = "2.5.0"
3
+ VERSION = "2.7.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppetserver-ca
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet, Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-14 00:00:00.000000000 Z
11
+ date: 2024-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: facter
@@ -80,6 +80,7 @@ executables:
80
80
  extensions: []
81
81
  extra_rdoc_files: []
82
82
  files:
83
+ - ".github/dependabot.yml"
83
84
  - ".github/workflows/mend.yaml"
84
85
  - ".gitignore"
85
86
  - ".rspec"
@@ -143,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
144
  - !ruby/object:Gem::Version
144
145
  version: '0'
145
146
  requirements: []
146
- rubygems_version: 3.0.9
147
+ rubygems_version: 3.4.20
147
148
  signing_key:
148
149
  specification_version: 4
149
150
  summary: A simple CLI tool for interacting with Puppet Server's Certificate Authority