chef-api 0.4.0 → 0.4.1
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 +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +7 -0
- data/Rakefile +10 -0
- data/chef-api.gemspec +2 -3
- data/lib/chef-api.rb +2 -0
- data/lib/chef-api/authentication.rb +298 -0
- data/lib/chef-api/connection.rb +29 -220
- data/lib/chef-api/multipart.rb +164 -0
- data/lib/chef-api/version.rb +1 -1
- data/spec/spec_helper.rb +7 -0
- data/spec/support/cookbook.tar.gz +0 -0
- data/spec/support/user.pem +27 -0
- data/spec/unit/authentication_spec.rb +70 -0
- metadata +10 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0f63c17b624c48909ad16a57e3aa8c104e5d8ca
|
4
|
+
data.tar.gz: 609d4327031d33d9816b1dc071e7df6ab70884f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa907ef2a5761997afc8a0c6b0a07f68d287f775b6c6ae3946be2ee52797cbfe26c023640d87662d9309457dc772db43e3d4cf1c1793e3111c679b67247e8bff
|
7
|
+
data.tar.gz: df493cd920ea8a6ab12b8d73e15c8fe4eceb8f1c4a7c339dbf1dc4c2be9c521b8f3e023ddfe80668120257c707e95995f6a9ca31e506d3c7f2a6f41ed4edd247
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
ChefAPI Changelog
|
2
2
|
=================
|
3
3
|
|
4
|
+
v0.4.1 (2014-07-07)
|
5
|
+
-------------------
|
6
|
+
- Remove dependency on mixlib-authentication
|
7
|
+
- Fix a bug where Content-Type headers were not sent properly
|
8
|
+
- Switch to rake for test running
|
9
|
+
- Improve test coverage with fixtures
|
10
|
+
|
4
11
|
v0.4.0 (2014-07-05)
|
5
12
|
-------------------
|
6
13
|
- Support multipart POST
|
data/Rakefile
CHANGED
data/chef-api.gemspec
CHANGED
@@ -20,7 +20,6 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
21
|
spec.require_paths = ['lib']
|
22
22
|
|
23
|
-
spec.add_dependency 'logify',
|
24
|
-
spec.add_dependency '
|
25
|
-
spec.add_dependency 'mime-types', '~> 2.3'
|
23
|
+
spec.add_dependency 'logify', '~> 0.1'
|
24
|
+
spec.add_dependency 'mime-types', '~> 2.3'
|
26
25
|
end
|
data/lib/chef-api.rb
CHANGED
@@ -7,12 +7,14 @@ require 'chef-api/version'
|
|
7
7
|
JSON.create_id = nil
|
8
8
|
|
9
9
|
module ChefAPI
|
10
|
+
autoload :Authentication, 'chef-api/authentication'
|
10
11
|
autoload :Boolean, 'chef-api/boolean'
|
11
12
|
autoload :Configurable, 'chef-api/configurable'
|
12
13
|
autoload :Connection, 'chef-api/connection'
|
13
14
|
autoload :Defaults, 'chef-api/defaults'
|
14
15
|
autoload :Error, 'chef-api/errors'
|
15
16
|
autoload :ErrorCollection, 'chef-api/error_collection'
|
17
|
+
autoload :Multipart, 'chef-api/multipart'
|
16
18
|
autoload :Resource, 'chef-api/resource'
|
17
19
|
autoload :Schema, 'chef-api/schema'
|
18
20
|
autoload :Util, 'chef-api/util'
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'digest'
|
3
|
+
require 'openssl'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
#
|
7
|
+
# DEBUG steps:
|
8
|
+
#
|
9
|
+
# check .chomp
|
10
|
+
#
|
11
|
+
|
12
|
+
module ChefAPI
|
13
|
+
class Authentication
|
14
|
+
include Logify
|
15
|
+
|
16
|
+
# @todo: Enable this in the future when Mixlib::Authentication supports
|
17
|
+
# signing the full request body instead of just the uploaded file parameter.
|
18
|
+
SIGN_FULL_BODY = false
|
19
|
+
|
20
|
+
SIGNATURE = 'algorithm=sha1;version=1.0;'.freeze
|
21
|
+
|
22
|
+
# Headers
|
23
|
+
X_OPS_SIGN = 'X-Ops-Sign'.freeze
|
24
|
+
X_OPS_USERID = 'X-Ops-Userid'.freeze
|
25
|
+
X_OPS_TIMESTAMP = 'X-Ops-Timestamp'.freeze
|
26
|
+
X_OPS_CONTENT_HASH = 'X-Ops-Content-Hash'.freeze
|
27
|
+
X_OPS_AUTHORIZATION = 'X-Ops-Authorization'.freeze
|
28
|
+
|
29
|
+
class << self
|
30
|
+
#
|
31
|
+
# Create a new signing object from the given options. All options are
|
32
|
+
# required.
|
33
|
+
#
|
34
|
+
# @see (#initialize)
|
35
|
+
#
|
36
|
+
# @option options [String] :user
|
37
|
+
# @option options [String, OpenSSL::PKey::RSA] :key
|
38
|
+
# @option options [String, Symbol] verb
|
39
|
+
# @option options [String] :path
|
40
|
+
# @option options [String, IO] :body
|
41
|
+
#
|
42
|
+
def from_options(options = {})
|
43
|
+
user = options.fetch(:user)
|
44
|
+
key = options.fetch(:key)
|
45
|
+
verb = options.fetch(:verb)
|
46
|
+
path = options.fetch(:path)
|
47
|
+
body = options.fetch(:body)
|
48
|
+
|
49
|
+
new(user, key, verb, path, body)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Create a new Authentication object for signing. Creating an instance will
|
55
|
+
# not run any validations or perform any operations (this is on purpose).
|
56
|
+
#
|
57
|
+
# @param [String] user
|
58
|
+
# the username/client/user of the user to sign the request. In Hosted
|
59
|
+
# Chef land, this is your "client". In Supermarket land, this is your
|
60
|
+
# "username".
|
61
|
+
# @param [String, OpenSSL::PKey::RSA] key
|
62
|
+
# the path to a private key on disk, the raw private key (as a String),
|
63
|
+
# or the raw private key (as an OpenSSL::PKey::RSA instance)
|
64
|
+
# @param [Symbol, String] verb
|
65
|
+
# the verb for the request (e.g. +:get+)
|
66
|
+
# @param [String] path
|
67
|
+
# the "path" part of the URI (e.g. +/path/to/resource+)
|
68
|
+
# @param [String, IO] body
|
69
|
+
# the body to sign for the request, as a raw string or an IO object to be
|
70
|
+
# read in chunks
|
71
|
+
#
|
72
|
+
def initialize(user, key, verb, path, body)
|
73
|
+
@user = user
|
74
|
+
@key = key
|
75
|
+
@verb = verb
|
76
|
+
@path = path
|
77
|
+
@body = body
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# The fully-qualified headers for this authentication object of the form:
|
82
|
+
#
|
83
|
+
# {
|
84
|
+
# 'X-Ops-Sign' => 'algorithm=sha1;version=1.1',
|
85
|
+
# 'X-Ops-Userid' => 'sethvargo',
|
86
|
+
# 'X-Ops-Timestamp' => '2014-07-07T02:17:15Z',
|
87
|
+
# 'X-Ops-Content-Hash' => '...',
|
88
|
+
# 'x-Ops-Authorization-1' => '...'
|
89
|
+
# 'x-Ops-Authorization-2' => '...'
|
90
|
+
# 'x-Ops-Authorization-3' => '...'
|
91
|
+
# # ...
|
92
|
+
# }
|
93
|
+
#
|
94
|
+
# @return [Hash]
|
95
|
+
# the signing headers
|
96
|
+
#
|
97
|
+
def headers
|
98
|
+
{
|
99
|
+
X_OPS_SIGN => SIGNATURE,
|
100
|
+
X_OPS_USERID => @user,
|
101
|
+
X_OPS_TIMESTAMP => canonical_timestamp,
|
102
|
+
X_OPS_CONTENT_HASH => content_hash,
|
103
|
+
}.merge(signature_lines)
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# The canonical body. This could be an IO object (such as +#body_stream+),
|
108
|
+
# an actual string (such as +#body+), or just the empty string if the
|
109
|
+
# request's body and stream was nil.
|
110
|
+
#
|
111
|
+
# @return [String, IO]
|
112
|
+
#
|
113
|
+
def content_hash
|
114
|
+
return @content_hash if @content_hash
|
115
|
+
|
116
|
+
if SIGN_FULL_BODY
|
117
|
+
@content_hash = hash(@body || '').chomp
|
118
|
+
else
|
119
|
+
if @body.is_a?(Multipart::MultiIO)
|
120
|
+
filepart = @body.ios.find { |io| io.is_a?(Multipart::MultiIO) }
|
121
|
+
file = filepart.ios.find { |io| io.is_a?(File) }
|
122
|
+
|
123
|
+
@content_hash = hash(file).chomp
|
124
|
+
else
|
125
|
+
@content_hash = hash(@body || '').chomp
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
@content_hash
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Parse the given private key. Users can specify the private key as:
|
134
|
+
#
|
135
|
+
# - the path to the key on disk
|
136
|
+
# - the raw string key
|
137
|
+
# - an +OpenSSL::PKey::RSA object+
|
138
|
+
#
|
139
|
+
# Any other implementations are not supported and will likely explode.
|
140
|
+
#
|
141
|
+
# @todo
|
142
|
+
# Handle errors when the file cannot be read due to insufficient
|
143
|
+
# permissions
|
144
|
+
#
|
145
|
+
# @return [OpenSSL::PKey::RSA]
|
146
|
+
# the RSA private key as an OpenSSL object
|
147
|
+
#
|
148
|
+
def canonical_key
|
149
|
+
return @canonical_key if @canonical_key
|
150
|
+
|
151
|
+
log.info "Parsing private key..."
|
152
|
+
|
153
|
+
if @key.nil?
|
154
|
+
log.warn "No private key given!"
|
155
|
+
raise 'No private key given!'
|
156
|
+
end
|
157
|
+
|
158
|
+
if @key.is_a?(OpenSSL::PKey::RSA)
|
159
|
+
log.debug "Detected private key is an OpenSSL Ruby object"
|
160
|
+
@canonical_key = @key
|
161
|
+
elsif @key =~ /(.+)\.pem$/ || File.exists?(File.expand_path(@key))
|
162
|
+
log.debug "Detected private key is the path to a file"
|
163
|
+
contents = File.read(File.expand_path(@key))
|
164
|
+
@canonical_key = OpenSSL::PKey::RSA.new(contents)
|
165
|
+
else
|
166
|
+
log.debug "Detected private key was the literal string key"
|
167
|
+
@canonical_key = OpenSSL::PKey::RSA.new(@key)
|
168
|
+
end
|
169
|
+
|
170
|
+
@canonical_key
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
#
|
175
|
+
# The canonical path, with duplicate and trailing slashes removed. This
|
176
|
+
# value is then hashed.
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# "/zip//zap/foo" #=> "/zip/zap/foo"
|
180
|
+
#
|
181
|
+
# @return [String]
|
182
|
+
#
|
183
|
+
def canonical_path
|
184
|
+
@canonical_path ||= hash(@path.squeeze('/').gsub(/(\/)+$/,'')).chomp
|
185
|
+
end
|
186
|
+
|
187
|
+
#
|
188
|
+
# The iso8601 timestamp for this request. This value must be cached so it
|
189
|
+
# is persisted throughout this entire request.
|
190
|
+
#
|
191
|
+
# @return [String]
|
192
|
+
#
|
193
|
+
def canonical_timestamp
|
194
|
+
@canonical_timestamp ||= Time.now.utc.iso8601
|
195
|
+
end
|
196
|
+
|
197
|
+
#
|
198
|
+
# The uppercase verb.
|
199
|
+
#
|
200
|
+
# @example
|
201
|
+
# :get #=> "GET"
|
202
|
+
#
|
203
|
+
# @return [String]
|
204
|
+
#
|
205
|
+
def canonical_method
|
206
|
+
@canonical_method ||= @verb.to_s.upcase
|
207
|
+
end
|
208
|
+
|
209
|
+
#
|
210
|
+
# The canonical request, from the path, body, user, and current timestamp.
|
211
|
+
#
|
212
|
+
# @return [String]
|
213
|
+
#
|
214
|
+
def canonical_request
|
215
|
+
[
|
216
|
+
"Method:#{canonical_method}",
|
217
|
+
"Hashed Path:#{canonical_path}",
|
218
|
+
"X-Ops-Content-Hash:#{content_hash}",
|
219
|
+
"X-Ops-Timestamp:#{canonical_timestamp}",
|
220
|
+
"X-Ops-UserId:#{@user}",
|
221
|
+
].join("\n")
|
222
|
+
end
|
223
|
+
|
224
|
+
#
|
225
|
+
# The canonical request, encrypted by the given private key.
|
226
|
+
#
|
227
|
+
# @return [String]
|
228
|
+
#
|
229
|
+
def encrypted_request
|
230
|
+
canonical_key.private_encrypt(canonical_request).chomp
|
231
|
+
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# The +X-Ops-Authorization-N+ headers. This method takes the encrypted
|
235
|
+
# request, splits on a newline, and creates a signed header authentication
|
236
|
+
# request. N begins at 1, not 0 because the original author of
|
237
|
+
# Mixlib::Authentication did not believe in computer science.
|
238
|
+
#
|
239
|
+
# @return [Hash]
|
240
|
+
#
|
241
|
+
def signature_lines
|
242
|
+
signature = Base64.encode64(encrypted_request)
|
243
|
+
signature.split(/\n/).each_with_index.inject({}) do |hash, (line, index)|
|
244
|
+
hash["#{X_OPS_AUTHORIZATION}-#{index + 1}"] = line
|
245
|
+
hash
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
private
|
250
|
+
|
251
|
+
#
|
252
|
+
# Hash the given object.
|
253
|
+
#
|
254
|
+
# @param [String, IO] object
|
255
|
+
# a string or IO object to hash
|
256
|
+
#
|
257
|
+
# @return [String]
|
258
|
+
# the hashed value
|
259
|
+
#
|
260
|
+
def hash(object)
|
261
|
+
if object.respond_to?(:read)
|
262
|
+
digest_io(object)
|
263
|
+
else
|
264
|
+
digest_string(object)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
#
|
269
|
+
# Digest the given IO, reading in 1024 bytes at one time.
|
270
|
+
#
|
271
|
+
# @param [IO] io
|
272
|
+
# the IO (or File object)
|
273
|
+
#
|
274
|
+
# @return [String]
|
275
|
+
#
|
276
|
+
def digest_io(io)
|
277
|
+
digester = Digest::SHA1.new
|
278
|
+
|
279
|
+
while buffer = io.read(1024)
|
280
|
+
digester.update(buffer)
|
281
|
+
end
|
282
|
+
|
283
|
+
Base64.encode64(digester.digest)
|
284
|
+
end
|
285
|
+
|
286
|
+
#
|
287
|
+
# Digest a string.
|
288
|
+
#
|
289
|
+
# @param [String] string
|
290
|
+
# the string to digest
|
291
|
+
#
|
292
|
+
# @return [String]
|
293
|
+
#
|
294
|
+
def digest_string(string)
|
295
|
+
Base64.encode64(Digest::SHA1.digest(string))
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
data/lib/chef-api/connection.rb
CHANGED
@@ -202,25 +202,35 @@ module ChefAPI
|
|
202
202
|
# Setup PATCH/POST/PUT
|
203
203
|
if [:patch, :post, :put].include?(verb)
|
204
204
|
if data.respond_to?(:read)
|
205
|
+
log.info "Detected file/io presence"
|
205
206
|
request.body_stream = data
|
206
207
|
elsif data.is_a?(Hash)
|
207
208
|
# If any of the values in the hash are File-like, assume this is a
|
208
209
|
# multi-part post
|
209
210
|
if data.values.any? { |value| value.respond_to?(:read) }
|
211
|
+
log.info "Detected multipart body"
|
212
|
+
|
210
213
|
multipart = Multipart::Body.new(data)
|
214
|
+
|
215
|
+
log.debug "Content-Type: #{multipart.content_type}"
|
216
|
+
log.debug "Content-Length: #{multipart.content_length}"
|
217
|
+
|
211
218
|
request.content_length = multipart.content_length
|
212
219
|
request.content_type = multipart.content_type
|
220
|
+
|
213
221
|
request.body_stream = multipart.stream
|
214
222
|
else
|
223
|
+
log.info "Detected form data"
|
215
224
|
request.form_data = data
|
216
225
|
end
|
217
226
|
else
|
227
|
+
log.info "Detected regular body"
|
218
228
|
request.body = data
|
219
229
|
end
|
220
230
|
end
|
221
231
|
|
222
232
|
# Sign the request
|
223
|
-
add_signing_headers(verb, uri, request
|
233
|
+
add_signing_headers(verb, uri.path, request)
|
224
234
|
|
225
235
|
# Create the HTTP connection object - since the proxy information defaults
|
226
236
|
# to +nil+, we can just pass it to the initializer method instead of doing
|
@@ -351,49 +361,6 @@ module ChefAPI
|
|
351
361
|
|
352
362
|
private
|
353
363
|
|
354
|
-
#
|
355
|
-
# Parse the given private key. Users can specify the private key as:
|
356
|
-
#
|
357
|
-
# - the path to the key on disk
|
358
|
-
# - the raw string key
|
359
|
-
# - an +OpenSSL::PKey::RSA object+
|
360
|
-
#
|
361
|
-
# Any other implementations are not supported and will likely explode.
|
362
|
-
#
|
363
|
-
# @todo
|
364
|
-
# Handle errors when the file cannot be read due to insufficient
|
365
|
-
# permissions
|
366
|
-
#
|
367
|
-
# @return [OpenSSL::PKey::RSA]
|
368
|
-
# the RSA private key as an OpenSSL object
|
369
|
-
#
|
370
|
-
def parsed_key
|
371
|
-
return @parsed_key if @parsed_key
|
372
|
-
|
373
|
-
log.info "Parsing private key..."
|
374
|
-
|
375
|
-
if key.nil?
|
376
|
-
log.warn "No private key given!"
|
377
|
-
raise 'No private key given!'
|
378
|
-
end
|
379
|
-
|
380
|
-
if key.is_a?(OpenSSL::PKey::RSA)
|
381
|
-
log.debug "Detected private key is an OpenSSL Ruby object"
|
382
|
-
@parsed_key = key
|
383
|
-
end
|
384
|
-
|
385
|
-
if key =~ /(.+)\.pem$/ || File.exists?(File.expand_path(key))
|
386
|
-
log.debug "Detected private key is the path to a file"
|
387
|
-
contents = File.read(File.expand_path(key))
|
388
|
-
@parsed_key = OpenSSL::PKey::RSA.new(contents)
|
389
|
-
else
|
390
|
-
log.debug "Detected private key was the literal string key"
|
391
|
-
@parsed_key = OpenSSL::PKey::RSA.new(key)
|
392
|
-
end
|
393
|
-
|
394
|
-
@parsed_key
|
395
|
-
end
|
396
|
-
|
397
364
|
#
|
398
365
|
# Parse the response object and manipulate the result based on the given
|
399
366
|
# +Content-Type+ header. For now, this method only parses JSON, but it
|
@@ -433,7 +400,7 @@ module ChefAPI
|
|
433
400
|
when /json/
|
434
401
|
log.debug "Detected error response as JSON"
|
435
402
|
log.debug "Parsing error response as JSON"
|
436
|
-
message =
|
403
|
+
message = JSON.parse(response.body)
|
437
404
|
else
|
438
405
|
log.debug "Detected response as text/plain"
|
439
406
|
message = response.body
|
@@ -485,191 +452,33 @@ module ChefAPI
|
|
485
452
|
end
|
486
453
|
|
487
454
|
#
|
488
|
-
#
|
455
|
+
# Create a signed header authentication that can be consumed by
|
456
|
+
# +Mixlib::Authentication+.
|
489
457
|
#
|
458
|
+
# @param [Symbol] verb
|
459
|
+
# the HTTP verb (e.g. +:get+)
|
460
|
+
# @param [String] path
|
461
|
+
# the requested URI path (e.g. +/resources/foo+)
|
490
462
|
# @param [Net::HTTP::Request] request
|
491
463
|
#
|
492
|
-
def add_signing_headers(verb,
|
464
|
+
def add_signing_headers(verb, path, request)
|
493
465
|
log.info "Adding signed header authentication..."
|
494
466
|
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
:
|
501
|
-
|
502
|
-
:host => "#{uri.host}:#{uri.port}",
|
503
|
-
:path => uri.path,
|
504
|
-
:timestamp => Time.now.utc.iso8601,
|
505
|
-
:user_id => client,
|
506
|
-
:file => '',
|
507
|
-
).sign(key)
|
467
|
+
authentication = Authentication.from_options(
|
468
|
+
user: client,
|
469
|
+
key: key,
|
470
|
+
verb: verb,
|
471
|
+
path: path,
|
472
|
+
body: request.body || request.body_stream,
|
473
|
+
)
|
508
474
|
|
509
|
-
headers.each do |key, value|
|
475
|
+
authentication.headers.each do |key, value|
|
510
476
|
log.debug "#{key}: #{value}"
|
511
477
|
request[key] = value
|
512
478
|
end
|
513
|
-
end
|
514
|
-
end
|
515
|
-
|
516
|
-
require 'cgi'
|
517
|
-
require 'mime/types'
|
518
|
-
|
519
|
-
module Multipart
|
520
|
-
BOUNDARY = '------ChefAPIMultipartBoundary'.freeze
|
521
|
-
|
522
|
-
class Body
|
523
|
-
def initialize(params = {})
|
524
|
-
params.each do |key, value|
|
525
|
-
if value.respond_to?(:read)
|
526
|
-
parts << FilePart.new(key, value)
|
527
|
-
else
|
528
|
-
parts << ParamPart.new(key, value)
|
529
|
-
end
|
530
|
-
end
|
531
|
-
|
532
|
-
parts << EndingPart.new
|
533
|
-
end
|
534
|
-
|
535
|
-
def stream
|
536
|
-
MultiIO.new(*parts.map(&:io))
|
537
|
-
end
|
538
|
-
|
539
|
-
def content_type
|
540
|
-
"multipart/form-data; boundary=#{BOUNDARY}"
|
541
|
-
end
|
542
|
-
|
543
|
-
def content_length
|
544
|
-
parts.map(&:size).inject(:+)
|
545
|
-
end
|
546
|
-
|
547
|
-
private
|
548
|
-
|
549
|
-
def parts
|
550
|
-
@parts ||= []
|
551
|
-
end
|
552
|
-
end
|
553
|
-
|
554
|
-
class MultiIO
|
555
|
-
def initialize(*ios)
|
556
|
-
@ios = ios
|
557
|
-
@index = 0
|
558
|
-
end
|
559
|
-
|
560
|
-
# Read from IOs in order until `length` bytes have been received.
|
561
|
-
def read(length = nil, outbuf = nil)
|
562
|
-
got_result = false
|
563
|
-
outbuf = outbuf ? outbuf.replace('') : ''
|
564
|
-
|
565
|
-
while io = current_io
|
566
|
-
if result = io.read(length)
|
567
|
-
got_result ||= !result.nil?
|
568
|
-
result.force_encoding('BINARY') if result.respond_to?(:force_encoding)
|
569
|
-
outbuf << result
|
570
|
-
length -= result.length if length
|
571
|
-
break if length == 0
|
572
|
-
end
|
573
|
-
advance_io
|
574
|
-
end
|
575
|
-
|
576
|
-
(!got_result && length) ? nil : outbuf
|
577
|
-
end
|
578
|
-
|
579
|
-
def rewind
|
580
|
-
@ios.each { |io| io.rewind }
|
581
|
-
@index = 0
|
582
|
-
end
|
583
|
-
|
584
|
-
private
|
585
|
-
|
586
|
-
def current_io
|
587
|
-
@ios[@index]
|
588
|
-
end
|
589
|
-
|
590
|
-
def advance_io
|
591
|
-
@index += 1
|
592
|
-
end
|
593
|
-
end
|
594
|
-
|
595
|
-
#
|
596
|
-
# A generic key => value part.
|
597
|
-
#
|
598
|
-
class ParamPart
|
599
|
-
def initialize(name, value)
|
600
|
-
@part = build(name, value)
|
601
|
-
end
|
602
|
-
|
603
|
-
def io
|
604
|
-
@io ||= StringIO.new(@part)
|
605
|
-
end
|
606
|
-
|
607
|
-
def size
|
608
|
-
@part.bytesize
|
609
|
-
end
|
610
|
-
|
611
|
-
private
|
612
|
-
|
613
|
-
def build(name, value)
|
614
|
-
part = %|--#{BOUNDARY}\r\n|
|
615
|
-
part << %|Content-Disposition: form-data; name="#{CGI.escape(name)}"\r\n\r\n|
|
616
|
-
part << %|#{value}\r\n|
|
617
|
-
part
|
618
|
-
end
|
619
|
-
end
|
620
|
-
|
621
|
-
#
|
622
|
-
# A File part
|
623
|
-
#
|
624
|
-
class FilePart
|
625
|
-
def initialize(name, file)
|
626
|
-
@file = file
|
627
|
-
@head = build(name, file)
|
628
|
-
@foot = "\r\n"
|
629
|
-
end
|
630
|
-
|
631
|
-
def io
|
632
|
-
@io ||= MultiIO.new(
|
633
|
-
StringIO.new(@head),
|
634
|
-
@file,
|
635
|
-
StringIO.new(@foot)
|
636
|
-
)
|
637
|
-
end
|
638
|
-
|
639
|
-
def size
|
640
|
-
@head.bytesize + @file.size + @foot.bytesize
|
641
|
-
end
|
642
|
-
|
643
|
-
private
|
644
|
-
|
645
|
-
def build(name, file)
|
646
|
-
filename = File.basename(file.path)
|
647
|
-
mime_type = MIME::Types.type_for(filename)[0] || MIME::Types['application/octet-stream'][0]
|
648
|
-
|
649
|
-
part = %|--#{BOUNDARY}\r\n|
|
650
|
-
part << %|Content-Disposition: form-data; name="#{CGI.escape(name)}"; filename="#{filename}"\r\n|
|
651
|
-
part << %|Content-Length: #{file.size}\r\n|
|
652
|
-
part << %|Content-Type: #{mime_type.simplified}|
|
653
|
-
part << %|Content-Transfer-Encoding: binary\r\n|
|
654
|
-
part << %|\r\n|
|
655
|
-
part
|
656
|
-
end
|
657
|
-
end
|
658
|
-
|
659
|
-
#
|
660
|
-
# The end of the entire request
|
661
|
-
#
|
662
|
-
class EndingPart
|
663
|
-
def initialize
|
664
|
-
@part = "--#{BOUNDARY}--\r\n\r\n"
|
665
|
-
end
|
666
|
-
|
667
|
-
def io
|
668
|
-
@io ||= StringIO.new(@part)
|
669
|
-
end
|
670
479
|
|
671
|
-
|
672
|
-
|
480
|
+
if request.body_stream
|
481
|
+
request.body_stream.rewind
|
673
482
|
end
|
674
483
|
end
|
675
484
|
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'mime/types'
|
3
|
+
|
4
|
+
module ChefAPI
|
5
|
+
module Multipart
|
6
|
+
BOUNDARY = '------ChefAPIMultipartBoundary'.freeze
|
7
|
+
|
8
|
+
class Body
|
9
|
+
def initialize(params = {})
|
10
|
+
params.each do |key, value|
|
11
|
+
if value.respond_to?(:read)
|
12
|
+
parts << FilePart.new(key, value)
|
13
|
+
else
|
14
|
+
parts << ParamPart.new(key, value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
parts << EndingPart.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def stream
|
22
|
+
MultiIO.new(*parts.map(&:io))
|
23
|
+
end
|
24
|
+
|
25
|
+
def content_type
|
26
|
+
"multipart/form-data; boundary=#{BOUNDARY}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def content_length
|
30
|
+
parts.map(&:size).inject(:+)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def parts
|
36
|
+
@parts ||= []
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class MultiIO
|
41
|
+
attr_reader :ios
|
42
|
+
|
43
|
+
def initialize(*ios)
|
44
|
+
@ios = ios
|
45
|
+
@index = 0
|
46
|
+
end
|
47
|
+
|
48
|
+
# Read from IOs in order until `length` bytes have been received.
|
49
|
+
def read(length = nil, outbuf = nil)
|
50
|
+
got_result = false
|
51
|
+
outbuf = outbuf ? outbuf.replace('') : ''
|
52
|
+
|
53
|
+
while io = current_io
|
54
|
+
if result = io.read(length)
|
55
|
+
got_result ||= !result.nil?
|
56
|
+
result.force_encoding('BINARY') if result.respond_to?(:force_encoding)
|
57
|
+
outbuf << result
|
58
|
+
length -= result.length if length
|
59
|
+
break if length == 0
|
60
|
+
end
|
61
|
+
advance_io
|
62
|
+
end
|
63
|
+
|
64
|
+
(!got_result && length) ? nil : outbuf
|
65
|
+
end
|
66
|
+
|
67
|
+
def rewind
|
68
|
+
@ios.each { |io| io.rewind }
|
69
|
+
@index = 0
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def current_io
|
75
|
+
@ios[@index]
|
76
|
+
end
|
77
|
+
|
78
|
+
def advance_io
|
79
|
+
@index += 1
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# A generic key => value part.
|
85
|
+
#
|
86
|
+
class ParamPart
|
87
|
+
def initialize(name, value)
|
88
|
+
@part = build(name, value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def io
|
92
|
+
@io ||= StringIO.new(@part)
|
93
|
+
end
|
94
|
+
|
95
|
+
def size
|
96
|
+
@part.bytesize
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def build(name, value)
|
102
|
+
part = %|--#{BOUNDARY}\r\n|
|
103
|
+
part << %|Content-Disposition: form-data; name="#{CGI.escape(name)}"\r\n\r\n|
|
104
|
+
part << %|#{value}\r\n|
|
105
|
+
part
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# A File part
|
111
|
+
#
|
112
|
+
class FilePart
|
113
|
+
def initialize(name, file)
|
114
|
+
@file = file
|
115
|
+
@head = build(name, file)
|
116
|
+
@foot = "\r\n"
|
117
|
+
end
|
118
|
+
|
119
|
+
def io
|
120
|
+
@io ||= MultiIO.new(
|
121
|
+
StringIO.new(@head),
|
122
|
+
@file,
|
123
|
+
StringIO.new(@foot)
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def size
|
128
|
+
@head.bytesize + @file.size + @foot.bytesize
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def build(name, file)
|
134
|
+
filename = File.basename(file.path)
|
135
|
+
mime_type = MIME::Types.type_for(filename)[0] || MIME::Types['application/octet-stream'][0]
|
136
|
+
|
137
|
+
part = %|--#{BOUNDARY}\r\n|
|
138
|
+
part << %|Content-Disposition: form-data; name="#{CGI.escape(name)}"; filename="#{filename}"\r\n|
|
139
|
+
part << %|Content-Length: #{file.size}\r\n|
|
140
|
+
part << %|Content-Type: #{mime_type.simplified}\r\n|
|
141
|
+
part << %|Content-Transfer-Encoding: binary\r\n|
|
142
|
+
part << %|\r\n|
|
143
|
+
part
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# The end of the entire request
|
149
|
+
#
|
150
|
+
class EndingPart
|
151
|
+
def initialize
|
152
|
+
@part = "--#{BOUNDARY}--\r\n\r\n"
|
153
|
+
end
|
154
|
+
|
155
|
+
def io
|
156
|
+
@io ||= StringIO.new(@part)
|
157
|
+
end
|
158
|
+
|
159
|
+
def size
|
160
|
+
@part.bytesize
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
data/lib/chef-api/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
Binary file
|
@@ -0,0 +1,27 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIEowIBAAKCAQEA1xzQLQxDANx/Yu73NbqATU898uvHcVaMglg4FjMMOhqLTE3g
|
3
|
+
MIDMUeBH05c167kRmF+6a3l3dNlDyt8cBtWo277Yfd3FPOzeaf/g7umKY0Ids9S4
|
4
|
+
fZBP+wcG8mpk2ReNiAcIwJlTZHWSdUSoHIZeDXd8eE5tU1WfdWeXfd+yQFVTv5fj
|
5
|
+
U0VwU/c4+UzMjySbHiv94mKIfels/M7hnlRoJyepJFLThTi5bE4bmyizhvBp6j8V
|
6
|
+
HbaLdSfBis1FgDuaajSjTu6m41X7HshSuWkRp1w+2PWQnXwoCIBIogHcTlE+UGt6
|
7
|
+
LNa23/jiMCzRGYQiS/gl5+6gTByhCcV7f2aXXwIDAQABAoIBADYZVvmdVdSHn7nf
|
8
|
+
42gtyUqoHSpUxcnpPFkjmqdqmy6ZsmK0SyennLsSrr22D6eC2bv6h0W0PKi0Y2pI
|
9
|
+
BiJp5ZeuPYAaIBqcb6s04Pr03Qrte87YNrXNb2/wanzY6Rf35m5JZpgZd3GSaAz6
|
10
|
+
AVV7LXgxjqoq/y+wHvRF40GS2p924BePIzSgwWld2X7s39YdgSrxk2vytuqU2B8m
|
11
|
+
iH5fhJDghO7MQCX5aa+6YgLAvP28mKTPBOz8kCbiWPgDPxu9NpI4WGgzGgZqofjZ
|
12
|
+
GIyqZcDQ15oILdi0awWaVAK6Ob/24QQm33QaHzKZTKaRzWUeIJpnqPcGndjRC4Yt
|
13
|
+
FN/yVKECgYEA7ahqlovJN1AeZ1tDqP9UiGHSKyJmo3psnv3+u5DV+/cUUwDvbLF5
|
14
|
+
atCaGWZbdd0oSejeNeNX+JKZZE5xxSrKmuFnQe9lljylrWEOZ0TBngi4MABVJvst
|
15
|
+
vUG21vYVEZGiQzWZqZ92zxJQB3V0hOPNsyyAKVnyKctwhSlO3slbYhcCgYEA57bz
|
16
|
+
ueFcwQsQuJFLK2fX6ZmMJD0bwMXMOZIb+1s/URAIEClFqJYnYJuT7GGthz8099FL
|
17
|
+
2HyrGScrTYL+ekrAFTHmx/hBLdPwYhCunyptUvPPQwU8+mEhKSsqVtjHKnFfyHRB
|
18
|
+
T2c8AZNQNXhMMeJYANY3Gm1/4catWJ6RF/U1qfkCgYEApDqFrZLbcYXD/NhsYRRQ
|
19
|
+
bg5rFbOoCcBH33bV2Pe1Z3DOcq1qxkm+BboxQuwgt8okVS6+n66C1Bs6NL6gkAeK
|
20
|
+
Co1ItZ+hK7itJKq1MVeqFHMiFMmmDlH0wZvvpYxX8tQYtSkNDtJLX7zf4MehxVNG
|
21
|
+
ilJuHiUx2v/iuaJaBkpPA/ECgYAxNXthOGkYXh848zJBj5Yc+Az5DTk9oUQT3eGv
|
22
|
+
adtyfbMYq4stmGXYcHHju4K8vEGld39iBGfZuaXKmk0s738HgUd/pEtDTkU4rk5H
|
23
|
+
Yx1AhqK3mv8uNT5zncUqGHODofwzd+z+ze/CbeSU1m1oEqeZ1eRx6ltEOYtKzLIH
|
24
|
+
on25EQKBgEpxoKGGbp6EpY/wGCONeapjB/27gRAdLB5Nh/9HCAfVfoM/K31ECmL9
|
25
|
+
ZWWiwM6U2Qlmh8jGrhN4su8hpsNGbjvZ+kpA0MqJnJQGr6Y7iiSKDtd+Xc1cLh1g
|
26
|
+
YtL+yxlvdE9ue8oZut4Mfn0xQg2sns+OYi7mWQpssKeR/faPcGkK
|
27
|
+
-----END RSA PRIVATE KEY-----
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ChefAPI
|
4
|
+
describe Authentication do
|
5
|
+
let(:user) { 'sethvargo' }
|
6
|
+
let(:key) { rspec_support_file('user.pem') }
|
7
|
+
let(:body) { nil }
|
8
|
+
let(:verb) { :get }
|
9
|
+
let(:path) { '/foo/bar' }
|
10
|
+
let(:timestamp) { '1991-07-23T03:00:54Z' }
|
11
|
+
|
12
|
+
let(:headers) { described_class.new(user, key, verb, path, body).headers }
|
13
|
+
|
14
|
+
before do
|
15
|
+
allow(Time).to receive_message_chain(:now, :utc, :iso8601)
|
16
|
+
.and_return(timestamp)
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when given a request with no body' do
|
20
|
+
let(:body) { nil }
|
21
|
+
|
22
|
+
it 'returns the signed headers' do
|
23
|
+
expect(headers['X-Ops-Sign']).to eq('algorithm=sha1;version=1.0;')
|
24
|
+
expect(headers['X-Ops-Userid']).to eq('sethvargo')
|
25
|
+
expect(headers['X-Ops-Timestamp']).to eq('1991-07-23T03:00:54Z')
|
26
|
+
expect(headers['X-Ops-Content-Hash']).to eq('2jmj7l5rSw0yVb/vlWAYkK/YBwk=')
|
27
|
+
expect(headers['X-Ops-Authorization-1']).to eq('UuadIvkZTeZDcFW6oNilet0QzTcP/9JsRhSjIKCiZiqUeBG9jz4mU9w+TWsm')
|
28
|
+
expect(headers['X-Ops-Authorization-2']).to eq('2R3IiEKOW0S+UZpN19tPZ3nTdUluEvguidnsjuM/UpHymgY7M560pN4idXt5')
|
29
|
+
expect(headers['X-Ops-Authorization-3']).to eq('MQYAEHhFHTOfBX8ihOPkA5gkbLw6ehftjL10W/7H3bTrl1tiHHkv2Lmz4e+e')
|
30
|
+
expect(headers['X-Ops-Authorization-4']).to eq('9dJNeNDYVEaR1Efj7B7rnKjSD6SvRdqq0gbwiTfE7P2B88yjnq+a9eEoYgG3')
|
31
|
+
expect(headers['X-Ops-Authorization-5']).to eq('lmNnVT5pqJPHiE1YFj1OITywAi/5pMzJCzYzVyWxQT+4r+SIRtRESrRFi1Re')
|
32
|
+
expect(headers['X-Ops-Authorization-6']).to eq('OfHqhynKfmxMHAxVLJbfdjH3yX8Z8bq3tGPbdXxYAw==')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'when given a request with a string body' do
|
37
|
+
let(:body) { '{ "some": { "json": true } }' }
|
38
|
+
|
39
|
+
it 'returns the signed headers' do
|
40
|
+
expect(headers['X-Ops-Sign']).to eq('algorithm=sha1;version=1.0;')
|
41
|
+
expect(headers['X-Ops-Userid']).to eq('sethvargo')
|
42
|
+
expect(headers['X-Ops-Timestamp']).to eq('1991-07-23T03:00:54Z')
|
43
|
+
expect(headers['X-Ops-Content-Hash']).to eq('D3+ox1HKmuzp3SLWiSU/5RdnbdY=')
|
44
|
+
expect(headers['X-Ops-Authorization-1']).to eq('fbV8dt51y832DJS0bfR1LJ+EF/HHiDEgqJawNZyKMkgMHZ0Bv78kQVtH73fS')
|
45
|
+
expect(headers['X-Ops-Authorization-2']).to eq('s3JQkMpZOwsNO8n2iduexmTthJe/JXG4sUgBKkS2qtKxpBy5snFSb6wD5ZuC')
|
46
|
+
expect(headers['X-Ops-Authorization-3']).to eq('VJuC1YpOF6bGM8CyUG0O0SZBZRFZVgyC5TFACJn8ymMIx0FznWSPLyvoSjsZ')
|
47
|
+
expect(headers['X-Ops-Authorization-4']).to eq('pdVOjhPV2+EQaj3c01dBFx5FSXgnBhWSmu2DCel/74TDt5RBraPcB4wczwpz')
|
48
|
+
expect(headers['X-Ops-Authorization-5']).to eq('VIeVqGMuQ71OE0tabej4OKyf1+BopOedxVH1+KF5ETisxqrNhmEtUY5WrmSS')
|
49
|
+
expect(headers['X-Ops-Authorization-6']).to eq('hjhiBXFdieV24Sojq6PKBhEEwpJqrPVP1lZNkRXdoA==')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when given an IO object' do
|
54
|
+
let(:body) { File.open(rspec_support_file('cookbook.tar.gz')) }
|
55
|
+
|
56
|
+
it 'returns the signed headers' do
|
57
|
+
expect(headers['X-Ops-Sign']).to eq('algorithm=sha1;version=1.0;')
|
58
|
+
expect(headers['X-Ops-Userid']).to eq('sethvargo')
|
59
|
+
expect(headers['X-Ops-Timestamp']).to eq('1991-07-23T03:00:54Z')
|
60
|
+
expect(headers['X-Ops-Content-Hash']).to eq('AWFSGfxiL2XltqdgSKCpdm84H9o=')
|
61
|
+
expect(headers['X-Ops-Authorization-1']).to eq('oRvANxtLQanzqdC28l0szONjTni9zLRBiybYNyxyxos7M1X3kSs5LknmMA/E')
|
62
|
+
expect(headers['X-Ops-Authorization-2']).to eq('i6Izk87dCcG3LLiGqRh0x/BoayS9SyoctdfMRR5ivrKRUzuQU9elHRpXnmjw')
|
63
|
+
expect(headers['X-Ops-Authorization-3']).to eq('7i/tlbLPrJQ/0+di9BU4m+BBD/vbh80KajmsaszxHx1wwNEBkNAymSLSDqXX')
|
64
|
+
expect(headers['X-Ops-Authorization-4']).to eq('gVAjNiaEzV9/EPQyGAYaU40SOdDwKzBthxgCpM9sfpfQsXj4Oj4SvSmO+4sy')
|
65
|
+
expect(headers['X-Ops-Authorization-5']).to eq('eJ0l7vpR0MyQqnhqbJHkQAGsG/HUhuhG0E9T7dClk08EB+sdsnDxr+5laei3')
|
66
|
+
expect(headers['X-Ops-Authorization-6']).to eq('YtCw2spOnumfdqx2hWvLmxR3y2eOuLBv77tZXUQ4Ug==')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chef-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Seth Vargo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-07-
|
11
|
+
date: 2014-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: logify
|
@@ -24,20 +24,6 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0.1'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: mixlib-authentication
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1.3'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.3'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: mime-types
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -68,12 +54,14 @@ files:
|
|
68
54
|
- Rakefile
|
69
55
|
- chef-api.gemspec
|
70
56
|
- lib/chef-api.rb
|
57
|
+
- lib/chef-api/authentication.rb
|
71
58
|
- lib/chef-api/boolean.rb
|
72
59
|
- lib/chef-api/configurable.rb
|
73
60
|
- lib/chef-api/connection.rb
|
74
61
|
- lib/chef-api/defaults.rb
|
75
62
|
- lib/chef-api/error_collection.rb
|
76
63
|
- lib/chef-api/errors.rb
|
64
|
+
- lib/chef-api/multipart.rb
|
77
65
|
- lib/chef-api/resource.rb
|
78
66
|
- lib/chef-api/resources/base.rb
|
79
67
|
- lib/chef-api/resources/client.rb
|
@@ -106,7 +94,10 @@ files:
|
|
106
94
|
- spec/integration/resources/user_spec.rb
|
107
95
|
- spec/spec_helper.rb
|
108
96
|
- spec/support/chef_server.rb
|
97
|
+
- spec/support/cookbook.tar.gz
|
109
98
|
- spec/support/shared/chef_api_resource.rb
|
99
|
+
- spec/support/user.pem
|
100
|
+
- spec/unit/authentication_spec.rb
|
110
101
|
- spec/unit/errors_spec.rb
|
111
102
|
- spec/unit/resources/base_spec.rb
|
112
103
|
- spec/unit/resources/client_spec.rb
|
@@ -166,7 +157,10 @@ test_files:
|
|
166
157
|
- spec/integration/resources/user_spec.rb
|
167
158
|
- spec/spec_helper.rb
|
168
159
|
- spec/support/chef_server.rb
|
160
|
+
- spec/support/cookbook.tar.gz
|
169
161
|
- spec/support/shared/chef_api_resource.rb
|
162
|
+
- spec/support/user.pem
|
163
|
+
- spec/unit/authentication_spec.rb
|
170
164
|
- spec/unit/errors_spec.rb
|
171
165
|
- spec/unit/resources/base_spec.rb
|
172
166
|
- spec/unit/resources/client_spec.rb
|