chef-api 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|