mss-sdk 1.0.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 +7 -0
- data/.yardopts +9 -0
- data/LICENSE.txt +0 -0
- data/README.md +192 -0
- data/bin/mss-rb +178 -0
- data/ca-bundle.crt +3554 -0
- data/lib/mss/core/async_handle.rb +89 -0
- data/lib/mss/core/cacheable.rb +76 -0
- data/lib/mss/core/client.rb +786 -0
- data/lib/mss/core/collection/simple.rb +81 -0
- data/lib/mss/core/collection/with_limit_and_next_token.rb +70 -0
- data/lib/mss/core/collection/with_next_token.rb +96 -0
- data/lib/mss/core/collection.rb +262 -0
- data/lib/mss/core/configuration.rb +527 -0
- data/lib/mss/core/credential_providers.rb +653 -0
- data/lib/mss/core/data.rb +251 -0
- data/lib/mss/core/deprecations.rb +83 -0
- data/lib/mss/core/endpoints.rb +36 -0
- data/lib/mss/core/http/connection_pool.rb +374 -0
- data/lib/mss/core/http/curb_handler.rb +150 -0
- data/lib/mss/core/http/handler.rb +88 -0
- data/lib/mss/core/http/net_http_handler.rb +144 -0
- data/lib/mss/core/http/patch.rb +98 -0
- data/lib/mss/core/http/request.rb +258 -0
- data/lib/mss/core/http/response.rb +80 -0
- data/lib/mss/core/indifferent_hash.rb +87 -0
- data/lib/mss/core/inflection.rb +55 -0
- data/lib/mss/core/ini_parser.rb +41 -0
- data/lib/mss/core/json_client.rb +46 -0
- data/lib/mss/core/json_parser.rb +75 -0
- data/lib/mss/core/json_request_builder.rb +34 -0
- data/lib/mss/core/json_response_parser.rb +78 -0
- data/lib/mss/core/lazy_error_classes.rb +107 -0
- data/lib/mss/core/log_formatter.rb +426 -0
- data/lib/mss/core/managed_file.rb +31 -0
- data/lib/mss/core/meta_utils.rb +44 -0
- data/lib/mss/core/model.rb +61 -0
- data/lib/mss/core/naming.rb +29 -0
- data/lib/mss/core/option_grammar.rb +737 -0
- data/lib/mss/core/options/json_serializer.rb +81 -0
- data/lib/mss/core/options/validator.rb +154 -0
- data/lib/mss/core/options/xml_serializer.rb +117 -0
- data/lib/mss/core/page_result.rb +74 -0
- data/lib/mss/core/policy.rb +938 -0
- data/lib/mss/core/query_client.rb +40 -0
- data/lib/mss/core/query_error_parser.rb +23 -0
- data/lib/mss/core/query_request_builder.rb +46 -0
- data/lib/mss/core/query_response_parser.rb +34 -0
- data/lib/mss/core/region.rb +84 -0
- data/lib/mss/core/region_collection.rb +79 -0
- data/lib/mss/core/resource.rb +412 -0
- data/lib/mss/core/resource_cache.rb +39 -0
- data/lib/mss/core/response.rb +214 -0
- data/lib/mss/core/response_cache.rb +49 -0
- data/lib/mss/core/rest_error_parser.rb +23 -0
- data/lib/mss/core/rest_json_client.rb +39 -0
- data/lib/mss/core/rest_request_builder.rb +153 -0
- data/lib/mss/core/rest_response_parser.rb +65 -0
- data/lib/mss/core/rest_xml_client.rb +46 -0
- data/lib/mss/core/service_interface.rb +82 -0
- data/lib/mss/core/signers/base.rb +45 -0
- data/lib/mss/core/signers/cloud_front.rb +55 -0
- data/lib/mss/core/signers/s3.rb +158 -0
- data/lib/mss/core/signers/version_2.rb +71 -0
- data/lib/mss/core/signers/version_3.rb +85 -0
- data/lib/mss/core/signers/version_3_https.rb +60 -0
- data/lib/mss/core/signers/version_4/chunk_signed_stream.rb +190 -0
- data/lib/mss/core/signers/version_4.rb +227 -0
- data/lib/mss/core/uri_escape.rb +43 -0
- data/lib/mss/core/xml/frame.rb +245 -0
- data/lib/mss/core/xml/frame_stack.rb +84 -0
- data/lib/mss/core/xml/grammar.rb +306 -0
- data/lib/mss/core/xml/parser.rb +69 -0
- data/lib/mss/core/xml/root_frame.rb +64 -0
- data/lib/mss/core/xml/sax_handlers/libxml.rb +46 -0
- data/lib/mss/core/xml/sax_handlers/nokogiri.rb +55 -0
- data/lib/mss/core/xml/sax_handlers/ox.rb +40 -0
- data/lib/mss/core/xml/sax_handlers/rexml.rb +46 -0
- data/lib/mss/core/xml/stub.rb +122 -0
- data/lib/mss/core.rb +602 -0
- data/lib/mss/errors.rb +161 -0
- data/lib/mss/rails.rb +194 -0
- data/lib/mss/s3/access_control_list.rb +262 -0
- data/lib/mss/s3/acl_object.rb +263 -0
- data/lib/mss/s3/acl_options.rb +200 -0
- data/lib/mss/s3/bucket.rb +757 -0
- data/lib/mss/s3/bucket_collection.rb +161 -0
- data/lib/mss/s3/bucket_lifecycle_configuration.rb +472 -0
- data/lib/mss/s3/bucket_region_cache.rb +51 -0
- data/lib/mss/s3/bucket_tag_collection.rb +110 -0
- data/lib/mss/s3/bucket_version_collection.rb +78 -0
- data/lib/mss/s3/cipher_io.rb +119 -0
- data/lib/mss/s3/client/xml.rb +265 -0
- data/lib/mss/s3/client.rb +2076 -0
- data/lib/mss/s3/config.rb +60 -0
- data/lib/mss/s3/cors_rule.rb +107 -0
- data/lib/mss/s3/cors_rule_collection.rb +193 -0
- data/lib/mss/s3/data_options.rb +190 -0
- data/lib/mss/s3/encryption_utils.rb +145 -0
- data/lib/mss/s3/errors.rb +93 -0
- data/lib/mss/s3/multipart_upload.rb +353 -0
- data/lib/mss/s3/multipart_upload_collection.rb +75 -0
- data/lib/mss/s3/object_collection.rb +355 -0
- data/lib/mss/s3/object_metadata.rb +102 -0
- data/lib/mss/s3/object_upload_collection.rb +76 -0
- data/lib/mss/s3/object_version.rb +153 -0
- data/lib/mss/s3/object_version_collection.rb +88 -0
- data/lib/mss/s3/paginated_collection.rb +74 -0
- data/lib/mss/s3/policy.rb +73 -0
- data/lib/mss/s3/prefix_and_delimiter_collection.rb +46 -0
- data/lib/mss/s3/prefixed_collection.rb +84 -0
- data/lib/mss/s3/presign_v4.rb +135 -0
- data/lib/mss/s3/presigned_post.rb +574 -0
- data/lib/mss/s3/region_detection.rb +75 -0
- data/lib/mss/s3/request.rb +61 -0
- data/lib/mss/s3/s3_object.rb +1795 -0
- data/lib/mss/s3/tree/branch_node.rb +67 -0
- data/lib/mss/s3/tree/child_collection.rb +103 -0
- data/lib/mss/s3/tree/leaf_node.rb +93 -0
- data/lib/mss/s3/tree/node.rb +21 -0
- data/lib/mss/s3/tree/parent.rb +86 -0
- data/lib/mss/s3/tree.rb +115 -0
- data/lib/mss/s3/uploaded_part.rb +81 -0
- data/lib/mss/s3/uploaded_part_collection.rb +83 -0
- data/lib/mss/s3/website_configuration.rb +101 -0
- data/lib/mss/s3.rb +161 -0
- data/lib/mss/version.rb +16 -0
- data/lib/mss-sdk.rb +2 -0
- data/lib/mss.rb +14 -0
- data/rails/init.rb +14 -0
- metadata +201 -0
@@ -0,0 +1,653 @@
|
|
1
|
+
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
#
|
8
|
+
# or in the "license" file accompanying this file. This file is
|
9
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
10
|
+
# ANY KIND, either express or implied. See the License for the specific
|
11
|
+
# language governing permissions and limitations under the License.
|
12
|
+
|
13
|
+
require 'set'
|
14
|
+
require 'net/http'
|
15
|
+
require 'timeout'
|
16
|
+
require 'thread'
|
17
|
+
require 'time'
|
18
|
+
require 'json'
|
19
|
+
|
20
|
+
module MSS
|
21
|
+
module Core
|
22
|
+
module CredentialProviders
|
23
|
+
|
24
|
+
# This module is mixed into the various credential provider
|
25
|
+
# classes. It provides a unified interface for getting
|
26
|
+
# credentials and refreshing them.
|
27
|
+
module Provider
|
28
|
+
|
29
|
+
# The list of possible keys in the hash returned by {#credentials}.
|
30
|
+
KEYS = Set[:access_key_id, :secret_access_key, :session_token]
|
31
|
+
|
32
|
+
# @return [Hash] Returns a hash of credentials containg at least
|
33
|
+
# the `:access_key_id` and `:secret_access_key`. The hash may
|
34
|
+
# also contain a `:session_token`.
|
35
|
+
#
|
36
|
+
# @raise [Errors::MissingCredentialsError] Raised when the
|
37
|
+
# `:access_key_id` or the `:secret_access_key` can not be found.
|
38
|
+
#
|
39
|
+
def credentials
|
40
|
+
raise Errors::MissingCredentialsError unless set?
|
41
|
+
@cached_credentials.dup
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Boolean] Returns true if has credentials and it contains
|
45
|
+
# at least the `:access_key_id` and `:secret_access_key`.
|
46
|
+
#
|
47
|
+
def set?
|
48
|
+
@cache_mutex ||= Mutex.new
|
49
|
+
unless @cached_credentials
|
50
|
+
@cache_mutex.synchronize do
|
51
|
+
@cached_credentials ||= get_credentials
|
52
|
+
end
|
53
|
+
end
|
54
|
+
!!(@cached_credentials[:access_key_id] &&
|
55
|
+
@cached_credentials[:secret_access_key])
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [String] Returns the MSS access key id.
|
59
|
+
# @raise (see #credentials)
|
60
|
+
def access_key_id
|
61
|
+
credentials[:access_key_id]
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [String] Returns the MSS secret access key.
|
65
|
+
# @raise (see #credentials)
|
66
|
+
def secret_access_key
|
67
|
+
credentials[:secret_access_key]
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [String,nil] Returns the MSS session token or nil if these
|
71
|
+
# are not session credentials.
|
72
|
+
# @raise (see #credentials)
|
73
|
+
def session_token
|
74
|
+
credentials[:session_token]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Clears out cached/memoized credentials. Causes the provider
|
78
|
+
# to refetch credentials from the source.
|
79
|
+
# @return [nil]
|
80
|
+
def refresh
|
81
|
+
@cached_credentials = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
# This method is called on a credential provider to fetch
|
87
|
+
# credentials. The credentials hash returned from this
|
88
|
+
# method will be cached until the client calls {#refresh}.
|
89
|
+
# @return [Hash]
|
90
|
+
def get_credentials
|
91
|
+
# should be defined in provider classes.
|
92
|
+
raise NotImplementedError
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
# The default credential provider makes a best effort to
|
98
|
+
# locate your MSS credentials. It checks a variety of locations
|
99
|
+
# in the following order:
|
100
|
+
#
|
101
|
+
# * Static credentials from MSS.config (e.g. MSS.config.access_key_id,
|
102
|
+
# MSS.config.secret_access_key)
|
103
|
+
#
|
104
|
+
# * The environment (e.g. ENV['MSS_ACCESS_KEY_ID'] or
|
105
|
+
# ENV['AMAZON_ACCESS_KEY_ID'])
|
106
|
+
#
|
107
|
+
# * EC2 metadata service (checks for credentials provided by
|
108
|
+
# roles for instances).
|
109
|
+
#
|
110
|
+
class DefaultProvider
|
111
|
+
|
112
|
+
include Provider
|
113
|
+
|
114
|
+
# (see StaticProvider#new)
|
115
|
+
def initialize static_credentials = {}
|
116
|
+
@providers = []
|
117
|
+
@providers << StaticProvider.new(static_credentials)
|
118
|
+
@providers << ENVProvider.new('MSS')
|
119
|
+
@providers << ENVProvider.new('MSS', :access_key_id => 'ACCESS_KEY', :secret_access_key => 'SECRET_KEY', :session_token => 'SESSION_TOKEN')
|
120
|
+
@providers << ENVProvider.new('AMAZON')
|
121
|
+
begin
|
122
|
+
if Dir.home
|
123
|
+
@providers << SharedCredentialFileProvider.new
|
124
|
+
end
|
125
|
+
rescue ArgumentError, NoMethodError
|
126
|
+
end
|
127
|
+
@providers << EC2Provider.new
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [Array<Provider>]
|
131
|
+
attr_reader :providers
|
132
|
+
|
133
|
+
def credentials
|
134
|
+
providers.each do |provider|
|
135
|
+
if provider.set?
|
136
|
+
return provider.credentials
|
137
|
+
end
|
138
|
+
end
|
139
|
+
raise Errors::MissingCredentialsError
|
140
|
+
end
|
141
|
+
|
142
|
+
def set?
|
143
|
+
providers.any?(&:set?)
|
144
|
+
end
|
145
|
+
|
146
|
+
def refresh
|
147
|
+
providers.each do |provider|
|
148
|
+
provider.refresh
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Static credentials are provided directly to config via
|
154
|
+
# `:access_key_id` and `:secret_access_key` (and optionally
|
155
|
+
# `:session_token`).
|
156
|
+
# @api private
|
157
|
+
class StaticProvider
|
158
|
+
|
159
|
+
include Provider
|
160
|
+
|
161
|
+
# @param [Hash] static_credentials
|
162
|
+
# @option static_credentials [required,String] :access_key_id
|
163
|
+
# @option static_credentials [required,String] :secret_access_key
|
164
|
+
# @option static_credentials [String] :session_token
|
165
|
+
def initialize static_credentials = {}
|
166
|
+
|
167
|
+
static_credentials.keys.each do |opt_name|
|
168
|
+
unless KEYS.include?(opt_name)
|
169
|
+
raise ArgumentError, "invalid option #{opt_name.inspect}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
@static_credentials = {}
|
174
|
+
KEYS.each do |opt_name|
|
175
|
+
if opt_value = static_credentials[opt_name]
|
176
|
+
@static_credentials[opt_name] = opt_value
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
# (see Provider#get_credentials)
|
183
|
+
def get_credentials
|
184
|
+
@static_credentials
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
# Fetches credentials from the environment (ENV). You construct
|
190
|
+
# an ENV provider with a prefix. Given the prefix "MSS"
|
191
|
+
# ENV will be checked for the following keys:
|
192
|
+
#
|
193
|
+
# * MSS_ACCESS_KEY_ID
|
194
|
+
# * MSS_SECRET_ACCESS_KEY
|
195
|
+
# * MSS_SESSION_TOKEN (optional)
|
196
|
+
#
|
197
|
+
class ENVProvider
|
198
|
+
|
199
|
+
include Provider
|
200
|
+
|
201
|
+
# @param [String] prefix The prefix to apply to the ENV variable.
|
202
|
+
def initialize(prefix, suffixes=Hash[KEYS.map{|key| [key, key.to_s.upcase]}])
|
203
|
+
@prefix = prefix
|
204
|
+
@suffixes = suffixes
|
205
|
+
end
|
206
|
+
|
207
|
+
# @return [String]
|
208
|
+
attr_reader :prefix
|
209
|
+
|
210
|
+
# (see Provider#get_credentials)
|
211
|
+
def get_credentials
|
212
|
+
credentials = {}
|
213
|
+
KEYS.each do |key|
|
214
|
+
if value = ENV["#{@prefix}_#{@suffixes[key]}"]
|
215
|
+
credentials[key] = value
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Merge in CredentialFileProvider credentials if
|
220
|
+
# a #{@prefix}_CREDENTIAL_FILE environment(ENV) variable is set
|
221
|
+
if ENV["#{@prefix}_CREDENTIAL_FILE"]
|
222
|
+
credentials.merge! CredentialFileProvider.new(ENV["#{@prefix}_CREDENTIAL_FILE"]).get_credentials
|
223
|
+
end
|
224
|
+
|
225
|
+
credentials
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
# This credential provider gets credentials from a credential file
|
231
|
+
# with the following format:
|
232
|
+
#
|
233
|
+
# AWSAccessKeyId=your_key
|
234
|
+
# AWSSecretKey=your_secret
|
235
|
+
#
|
236
|
+
class CredentialFileProvider
|
237
|
+
|
238
|
+
include Provider
|
239
|
+
|
240
|
+
# Map of MSS credential file key names to accepted provider key names
|
241
|
+
CREDENTIAL_FILE_KEY_MAP = { "AWSAccessKeyId" => :access_key_id, "AWSSecretKey" => :secret_access_key }
|
242
|
+
|
243
|
+
attr_reader :credential_file
|
244
|
+
|
245
|
+
# @param [String] credential_file The file path of a credential file
|
246
|
+
def initialize(credential_file)
|
247
|
+
@credential_file = credential_file
|
248
|
+
end
|
249
|
+
|
250
|
+
# (see Provider#get_credentials)
|
251
|
+
def get_credentials
|
252
|
+
credentials = {}
|
253
|
+
if File.exist?(credential_file) && File.readable?(credential_file)
|
254
|
+
File.open(credential_file, 'r') do |fh|
|
255
|
+
fh.each_line do |line|
|
256
|
+
key, val = line.strip.split(%r(\s*=\s*))
|
257
|
+
if key && val && CREDENTIAL_FILE_KEY_MAP[key] && KEYS.include?(CREDENTIAL_FILE_KEY_MAP[key])
|
258
|
+
credentials[CREDENTIAL_FILE_KEY_MAP[key]] = val
|
259
|
+
end
|
260
|
+
end
|
261
|
+
fh.close
|
262
|
+
end
|
263
|
+
end
|
264
|
+
credentials
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class SharedCredentialFileProvider
|
269
|
+
|
270
|
+
include Provider
|
271
|
+
|
272
|
+
def shared_credential_file_path
|
273
|
+
if RUBY_VERSION < '1.9'
|
274
|
+
msg = "Must specify the :path to your shared credential file when using"
|
275
|
+
msg << " Ruby #{RUBY_VERSION}"
|
276
|
+
raise ArgumentError, msg
|
277
|
+
else
|
278
|
+
File.join(Dir.home, '.mss', 'credentials')
|
279
|
+
end
|
280
|
+
end
|
281
|
+
# @api private
|
282
|
+
KEY_MAP = {
|
283
|
+
"mss_access_key_id" => :access_key_id,
|
284
|
+
"mss_secret_access_key" => :secret_access_key,
|
285
|
+
"mss_session_token" => :session_token,
|
286
|
+
}
|
287
|
+
|
288
|
+
# @option [String] :path
|
289
|
+
# @option [String] :profile_name
|
290
|
+
def initialize(options = {})
|
291
|
+
@path = options[:path] || shared_credential_file_path
|
292
|
+
@profile_name = options[:profile_name]
|
293
|
+
@profile_name ||= ENV['MSS_PROFILE']
|
294
|
+
@profile_name ||= 'default'
|
295
|
+
end
|
296
|
+
|
297
|
+
# @return [String]
|
298
|
+
attr_reader :path
|
299
|
+
|
300
|
+
# @return [String]
|
301
|
+
attr_reader :profile_name
|
302
|
+
|
303
|
+
# (see Provider#get_credentials)
|
304
|
+
def get_credentials
|
305
|
+
if File.exist?(path) && File.readable?(path)
|
306
|
+
load_from_path
|
307
|
+
else
|
308
|
+
{}
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
private
|
313
|
+
|
314
|
+
def load_from_path
|
315
|
+
profile = load_profile
|
316
|
+
KEY_MAP.inject({}) do |credentials, (source, target)|
|
317
|
+
credentials[target] = profile[source] if profile.key?(source)
|
318
|
+
credentials
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def load_profile
|
323
|
+
ini = IniParser.parse(File.read(path))
|
324
|
+
ini[profile_name] || {}
|
325
|
+
end
|
326
|
+
|
327
|
+
end
|
328
|
+
|
329
|
+
# This credential provider tries to get credentials from the EC2
|
330
|
+
# metadata service.
|
331
|
+
class EC2Provider
|
332
|
+
|
333
|
+
# Raised when an http response is recieved with a non 200
|
334
|
+
# http status code.
|
335
|
+
# @api private
|
336
|
+
class FailedRequestError < StandardError; end
|
337
|
+
|
338
|
+
# These are the errors we trap when attempting to talk to the
|
339
|
+
# instance metadata service. Any of these imply the service
|
340
|
+
# is not present, no responding or some other non-recoverable
|
341
|
+
# error.
|
342
|
+
# @api private
|
343
|
+
FAILURES = [
|
344
|
+
FailedRequestError,
|
345
|
+
Errno::EHOSTUNREACH,
|
346
|
+
Errno::ECONNREFUSED,
|
347
|
+
SocketError,
|
348
|
+
Timeout::Error,
|
349
|
+
]
|
350
|
+
|
351
|
+
include Provider
|
352
|
+
|
353
|
+
# @param [Hash] options
|
354
|
+
# @option options [String] :ip_address ('169.254.169.254')
|
355
|
+
# @option options [Integer] :port (80)
|
356
|
+
# @option options [Integer] :retries (0) Number of times to
|
357
|
+
# retry retrieving credentials.
|
358
|
+
# @option options [Float] :http_open_timeout (1)
|
359
|
+
# @option options [Float] :http_read_timeout (1)
|
360
|
+
# @option options [Object] :http_debug_output (nil) HTTP wire
|
361
|
+
# traces are sent to this object. You can specify something
|
362
|
+
# like $stdout.
|
363
|
+
def initialize options = {}
|
364
|
+
@ip_address = options[:ip_address] || '169.254.169.254'
|
365
|
+
@port = options[:port] || 80
|
366
|
+
@retries = options[:retries] || 0
|
367
|
+
@http_open_timeout = options[:http_open_timeout] || 1
|
368
|
+
@http_read_timeout = options[:http_read_timeout] || 1
|
369
|
+
@http_debug_output = options[:http_debug_output]
|
370
|
+
end
|
371
|
+
|
372
|
+
# @return [String] Defaults to '169.254.169.254'.
|
373
|
+
attr_accessor :ip_address
|
374
|
+
|
375
|
+
# @return [Integer] Defaults to port 80.
|
376
|
+
attr_accessor :port
|
377
|
+
|
378
|
+
# @return [Integer] Defaults to 0
|
379
|
+
attr_accessor :retries
|
380
|
+
|
381
|
+
# @return [Float]
|
382
|
+
attr_accessor :http_open_timeout
|
383
|
+
|
384
|
+
# @return [Float]
|
385
|
+
attr_accessor :http_read_timeout
|
386
|
+
|
387
|
+
# @return [Object,nil]
|
388
|
+
attr_accessor :http_debug_output
|
389
|
+
|
390
|
+
# @return [Time,nil]
|
391
|
+
attr_accessor :credentials_expiration
|
392
|
+
|
393
|
+
# Refresh provider if existing credentials will be expired in 15 min
|
394
|
+
# @return [Hash] Returns a hash of credentials containg at least
|
395
|
+
# the `:access_key_id` and `:secret_access_key`. The hash may
|
396
|
+
# also contain a `:session_token`.
|
397
|
+
#
|
398
|
+
# @raise [Errors::MissingCredentialsError] Raised when the
|
399
|
+
# `:access_key_id` or the `:secret_access_key` can not be found.
|
400
|
+
#
|
401
|
+
def credentials
|
402
|
+
if @credentials_expiration && @credentials_expiration.utc <= (Time.now.utc + (15 * 60))
|
403
|
+
refresh
|
404
|
+
end
|
405
|
+
super
|
406
|
+
end
|
407
|
+
|
408
|
+
protected
|
409
|
+
|
410
|
+
# (see Provider#get_credentials)
|
411
|
+
def get_credentials
|
412
|
+
retries_left = retries
|
413
|
+
|
414
|
+
begin
|
415
|
+
|
416
|
+
http = Net::HTTP.new(ip_address, port, nil)
|
417
|
+
http.open_timeout = http_open_timeout
|
418
|
+
http.read_timeout = http_read_timeout
|
419
|
+
http.set_debug_output(http_debug_output) if
|
420
|
+
http_debug_output
|
421
|
+
http.start
|
422
|
+
|
423
|
+
# get the first/default instance profile name
|
424
|
+
path = '/latest/meta-data/iam/security-credentials/'
|
425
|
+
profile_name = get(http, path).lines.map(&:strip).first
|
426
|
+
|
427
|
+
# get the session details from the instance profile name
|
428
|
+
path << profile_name
|
429
|
+
session = JSON.parse(get(http, path))
|
430
|
+
|
431
|
+
http.finish
|
432
|
+
|
433
|
+
credentials = {}
|
434
|
+
credentials[:access_key_id] = session['AccessKeyId']
|
435
|
+
credentials[:secret_access_key] = session['SecretAccessKey']
|
436
|
+
credentials[:session_token] = session['Token']
|
437
|
+
@credentials_expiration = Time.parse(session['Expiration'])
|
438
|
+
|
439
|
+
credentials
|
440
|
+
|
441
|
+
rescue *FAILURES => e
|
442
|
+
if retries_left > 0
|
443
|
+
sleep_time = 2 ** (retries - retries_left)
|
444
|
+
Kernel.sleep(sleep_time)
|
445
|
+
|
446
|
+
retries_left -= 1
|
447
|
+
retry
|
448
|
+
else
|
449
|
+
{}
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Makes an HTTP Get request with the given path. If a non-200
|
455
|
+
# response is received, then a FailedRequestError is raised.
|
456
|
+
# a {FailedRequestError} is raised.
|
457
|
+
# @param [Net::HTTPSession] session
|
458
|
+
# @param [String] path
|
459
|
+
# @raise [FailedRequestError]
|
460
|
+
# @return [String] Returns the http response body.
|
461
|
+
def get session, path
|
462
|
+
response = session.request(Net::HTTP::Get.new(path))
|
463
|
+
if response.code.to_i == 200
|
464
|
+
response.body
|
465
|
+
else
|
466
|
+
raise FailedRequestError
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
end
|
471
|
+
|
472
|
+
# # Session Credential Provider
|
473
|
+
#
|
474
|
+
# The session provider consumes long term credentials (`:access_key_id`
|
475
|
+
# and `:secret_access_key`) and requests a session from STS.
|
476
|
+
# It then returns the short term credential set from STS.
|
477
|
+
#
|
478
|
+
# Calling {#refresh} causes the session provider to request a new
|
479
|
+
# set of credentials.
|
480
|
+
#
|
481
|
+
# This session provider is currently only used for DynamoDB which
|
482
|
+
# requires session credentials.
|
483
|
+
class SessionProvider
|
484
|
+
|
485
|
+
include Provider
|
486
|
+
|
487
|
+
@create_mutex = Mutex.new
|
488
|
+
|
489
|
+
class << self
|
490
|
+
|
491
|
+
# @param [Hash] long_term_credentials A hash of credentials with
|
492
|
+
# `:access_key_id` and `:secret_access_key` (but not
|
493
|
+
# `:session_token`).
|
494
|
+
def for long_term_credentials
|
495
|
+
@create_mutex.synchronize do
|
496
|
+
@session_providers ||= {}
|
497
|
+
@session_providers[long_term_credentials[:access_key_id]] =
|
498
|
+
self.new(long_term_credentials)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# Creation of SessionProviders *must* happen behind the mutex and
|
503
|
+
# we want to reuse session providers for the same access key id.
|
504
|
+
protected :new
|
505
|
+
|
506
|
+
end
|
507
|
+
|
508
|
+
# @param [Hash] long_term_credentials A hash of credentials with
|
509
|
+
# `:access_key_id` and `:secret_access_key` (but not
|
510
|
+
# `:session_token`).
|
511
|
+
def initialize long_term_credentials
|
512
|
+
@static = StaticProvider.new(long_term_credentials)
|
513
|
+
if @static.session_token
|
514
|
+
raise ArgumentError, 'invalid option :session_token'
|
515
|
+
end
|
516
|
+
@session_mutex = Mutex.new
|
517
|
+
end
|
518
|
+
|
519
|
+
# Aliasing the refresh method so we can call it from the refresh
|
520
|
+
# method defined in this class.
|
521
|
+
alias_method :orig_refresh, :refresh
|
522
|
+
protected :orig_refresh
|
523
|
+
|
524
|
+
# (see Provider#refresh)
|
525
|
+
def refresh
|
526
|
+
refresh_session
|
527
|
+
orig_refresh
|
528
|
+
end
|
529
|
+
|
530
|
+
protected
|
531
|
+
|
532
|
+
# (see Provider#get_credentials)
|
533
|
+
def get_credentials
|
534
|
+
session = cached_session
|
535
|
+
if session.nil?
|
536
|
+
refresh_session
|
537
|
+
session = cached_session
|
538
|
+
end
|
539
|
+
session.credentials
|
540
|
+
end
|
541
|
+
|
542
|
+
# Replaces the cached STS session with a new one.
|
543
|
+
# @return [nil]
|
544
|
+
def refresh_session
|
545
|
+
sts = MSS::STS.new(@static.credentials.merge(:use_ssl => true))
|
546
|
+
@session_mutex.synchronize do
|
547
|
+
@session = sts.new_session
|
548
|
+
end
|
549
|
+
nil
|
550
|
+
end
|
551
|
+
|
552
|
+
# @return [nil,STS::Session] Returns nil if a session has not
|
553
|
+
# already been started.
|
554
|
+
def cached_session
|
555
|
+
local_session = nil
|
556
|
+
@session_mutex.synchronize do
|
557
|
+
local_session = @session
|
558
|
+
end
|
559
|
+
local_session
|
560
|
+
end
|
561
|
+
|
562
|
+
end
|
563
|
+
|
564
|
+
# An auto-refreshing credential provider that works by assuming
|
565
|
+
# a role via {MSS::STS#assume_role}.
|
566
|
+
#
|
567
|
+
# provider = MSS::Core::CredentialProviders::AssumeRoleProvider.new(
|
568
|
+
# sts: MSS::STS.new(access_key_id:'AKID', secret_access_key:'SECRET'),
|
569
|
+
# # assume role options:
|
570
|
+
# role_arn: "linked::account::arn",
|
571
|
+
# role_session_name: "session-name"
|
572
|
+
# )
|
573
|
+
#
|
574
|
+
# ec2 = MSS::EC2.new(credential_provider:provider)
|
575
|
+
#
|
576
|
+
# If you omit the `:sts` option, a new {STS} service object will be
|
577
|
+
# constructed and it will use the default credential provider
|
578
|
+
# from {mss.config}.
|
579
|
+
#
|
580
|
+
class AssumeRoleProvider
|
581
|
+
|
582
|
+
include Provider
|
583
|
+
|
584
|
+
# @option options [MSS::STS] :sts (STS.new) An instance of {MSS::STS}.
|
585
|
+
# This is used to make the API call to assume role.
|
586
|
+
# @option options [required, String] :role_arn
|
587
|
+
# @option options [required, String] :role_session_name
|
588
|
+
# @option options [String] :policy
|
589
|
+
# @option options [Integer] :duration_seconds
|
590
|
+
# @option options [String] :external_id
|
591
|
+
def initialize(options = {})
|
592
|
+
@options = options.dup
|
593
|
+
@sts = @options.delete(:sts) || STS.new
|
594
|
+
end
|
595
|
+
|
596
|
+
def credentials
|
597
|
+
refresh if near_expiration?
|
598
|
+
super
|
599
|
+
end
|
600
|
+
|
601
|
+
private
|
602
|
+
|
603
|
+
def near_expiration?
|
604
|
+
@expiration && @expiration.utc <= Time.now.utc + 5 * 60
|
605
|
+
end
|
606
|
+
|
607
|
+
def get_credentials
|
608
|
+
role = @sts.assume_role(@options)
|
609
|
+
@expiration = role[:credentials][:expiration]
|
610
|
+
role[:credentials]
|
611
|
+
end
|
612
|
+
|
613
|
+
end
|
614
|
+
|
615
|
+
# Returns a set of fake credentials, should only be used for testing.
|
616
|
+
class FakeProvider < StaticProvider
|
617
|
+
|
618
|
+
# @param [Hash] options
|
619
|
+
# @option options [Boolean] :with_session_token (false) When `true` a
|
620
|
+
# fake session token will also be provided.
|
621
|
+
def initialize options = {}
|
622
|
+
options[:access_key_id] ||= fake_access_key_id
|
623
|
+
options[:secret_access_key] ||= fake_secret_access_key
|
624
|
+
if options.delete(:with_session_token)
|
625
|
+
options[:session_token] ||= fake_session_token
|
626
|
+
end
|
627
|
+
super
|
628
|
+
end
|
629
|
+
|
630
|
+
protected
|
631
|
+
|
632
|
+
def fake_access_key_id
|
633
|
+
"AKIA" + random_chars(16).upcase
|
634
|
+
end
|
635
|
+
|
636
|
+
def fake_secret_access_key
|
637
|
+
random_chars(40)
|
638
|
+
end
|
639
|
+
|
640
|
+
def fake_session_token
|
641
|
+
random_chars(260)
|
642
|
+
end
|
643
|
+
|
644
|
+
def random_chars count
|
645
|
+
chars = ('0'..'9').to_a + ('a'..'z').to_a + ('A'..'Z').to_a
|
646
|
+
(1..count).map{ chars[rand(chars.size)] }.join
|
647
|
+
end
|
648
|
+
|
649
|
+
end
|
650
|
+
|
651
|
+
end
|
652
|
+
end
|
653
|
+
end
|