elasticity 5.0.1 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 214e0a6224f1c25d0cab456008171d242f5b2cb6
4
- data.tar.gz: 417acbc3a8b312eabea4a86d4db2b1b9f726e8fa
3
+ metadata.gz: fd9e39257ac81bf6cb0a18f38d680bc0e6bdc5f1
4
+ data.tar.gz: d6f3dd0970a81a164e548e52707b29419f3ce107
5
5
  SHA512:
6
- metadata.gz: 856569d07a95865b11b3d54ebbbdad531b646200fd1df2eb609b75f408eb1be2d1b2eae0bc5625da1a222ebfed5c0e8b4eefc54dd36a4df0e66f0f43f290e5b3
7
- data.tar.gz: c46610d0917cd5089dccc6aa8712007d536b3a3ed5f94acb8cc7328a4c07e218efe3000c26f32508a03dae53ca28b98eae9b8de2df8d65e2d363e8bb4ac0ea06
6
+ metadata.gz: b47d9a75474f3afb40a34c9f0f7bd846fa250e0e15551b3106cd92cd804a710e8c818ac2770b65f9c57e5760d985c5a641db668094dcb9c57f0c2a694866634e
7
+ data.tar.gz: c6d00853cf474fb771bde33fdb50828ec9923d9918d059b06409fdd6ab7709184bf71db78943df23df1b30a7710504b574ccb3687b0cdf7bbf3d6c961752a8e1
data/HISTORY.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 5.0.2 - April 27, 2015
2
+
3
+ - Fix for issue [#83](https://github.com/rslifka/elasticity/issues/83), `elasticity` has now transitioned to the AWS [Signature Version 4 Signing Process](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html).
4
+ - Removed the ability to create insecure (HTTP) connections to the EMR endpoints.
5
+
1
6
  ## 5.0.1 - April 12, 2015
2
7
 
3
8
  - Bear with me here :) Backmerged into 4.0.4 to add [IAM Service Role support](http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/emr-iam-roles-creatingroles.html) per @alexanderdean. As part of the forward merge, bumping the version to trigger an update.
@@ -5,7 +5,12 @@ require 'rest_client'
5
5
  require 'nokogiri'
6
6
  require 'fog'
7
7
 
8
- require 'elasticity/aws_request'
8
+ require 'elasticity/version'
9
+
10
+ require 'elasticity/aws_utils'
11
+ require 'elasticity/aws_session'
12
+ require 'elasticity/aws_request_v2'
13
+ require 'elasticity/aws_request_v4'
9
14
  require 'elasticity/emr'
10
15
 
11
16
  require 'elasticity/sync_to_s3'
@@ -0,0 +1,42 @@
1
+ module Elasticity
2
+
3
+ class AwsRequestV2
4
+
5
+ def initialize(aws_session, ruby_service_hash)
6
+ @aws_session = aws_session
7
+ @ruby_service_hash = ruby_service_hash
8
+ end
9
+
10
+ def url
11
+ "https://elasticmapreduce.#{@aws_session.region}.amazonaws.com"
12
+ end
13
+
14
+ def headers
15
+ {
16
+ :content_type => 'application/x-www-form-urlencoded; charset=utf-8'
17
+ }
18
+ end
19
+
20
+ # (Used from RightScale's right_aws gem.)
21
+ # EC2, SQS, SDB and EMR requests must be signed by this guy.
22
+ # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
23
+ # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
24
+ def payload
25
+ service_hash = AwsUtils.convert_ruby_to_aws(@ruby_service_hash)
26
+ service_hash.merge!({
27
+ 'AWSAccessKeyId' => @aws_session.access_key,
28
+ 'Timestamp' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
29
+ 'SignatureVersion' => '2',
30
+ 'SignatureMethod' => 'HmacSHA256'
31
+ })
32
+ canonical_string = service_hash.keys.sort.map do |key|
33
+ "#{AwsUtils.aws_escape(key)}=#{AwsUtils.aws_escape(service_hash[key])}"
34
+ end.join('&')
35
+ string_to_sign = "POST\n#{@aws_session.host.downcase}\n/\n#{canonical_string}"
36
+ signature = AwsUtils.aws_escape(Base64.encode64(OpenSSL::HMAC.digest('sha256', @aws_session.secret_key, string_to_sign)).strip)
37
+ "#{canonical_string}&Signature=#{signature}"
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,98 @@
1
+ module Elasticity
2
+
3
+ # To help ensure correctness, Amazon has provided a step-by-step guide of
4
+ # query-and-response conversations for various types of API calls.
5
+ #
6
+ # http://docs.aws.amazon.com/general/latest/gr/signature-v4-test-suite.html
7
+ #
8
+ # We are working with POSTs only, where the body of the POST contains the
9
+ # service details, so the 'post-x-www-form-urlencoded-parameters' suite is
10
+ # the most applicable.
11
+ class AwsRequestV4
12
+
13
+ SERVICE_NAME = 'elasticmapreduce'
14
+
15
+ def initialize(aws_session, ruby_service_hash)
16
+ @aws_session = aws_session
17
+
18
+ @ruby_service_hash = ruby_service_hash
19
+ @operation = @ruby_service_hash[:operation]
20
+ @ruby_service_hash.delete(:operation)
21
+
22
+ @timestamp = Time.now.utc
23
+ end
24
+
25
+ def headers
26
+ {
27
+ 'Authorization' => "AWS4-HMAC-SHA256 Credential=#{@aws_session.access_key}/#{credential_scope}, SignedHeaders=content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=#{aws_v4_signature}",
28
+ 'Content-Type' => 'application/x-amz-json-1.1',
29
+ 'Host' => host,
30
+ 'User-Agent' => "elasticity/#{Elasticity::VERSION}",
31
+ 'X-Amz-Content-SHA256' => Digest::SHA256.hexdigest(payload),
32
+ 'X-Amz-Date' => @timestamp.strftime('%Y%m%dT%H%M%SZ'),
33
+ 'X-Amz-Target' => "ElasticMapReduce.#{@operation}",
34
+ }
35
+ end
36
+
37
+ def url
38
+ "https://#{host}"
39
+ end
40
+
41
+ def payload
42
+ AwsUtils.convert_ruby_to_aws_v4(@ruby_service_hash).to_json
43
+ end
44
+
45
+ private
46
+
47
+ def host
48
+ "elasticmapreduce.#{@aws_session.region}.amazonaws.com"
49
+ end
50
+
51
+ def credential_scope
52
+ "#{@timestamp.strftime('%Y%m%d')}/#{@aws_session.region}/#{SERVICE_NAME}/aws4_request"
53
+ end
54
+
55
+ # Task 1: Create a Canonical Request For Signature Version 4
56
+ # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
57
+ def canonical_request
58
+ [
59
+ 'POST',
60
+ '/',
61
+ '',
62
+ 'content-type:application/x-amz-json-1.1',
63
+ "host:#{host}",
64
+ "user-agent:elasticity/#{Elasticity::VERSION}",
65
+ "x-amz-content-sha256:#{Digest::SHA256.hexdigest(payload)}",
66
+ "x-amz-date:#{@timestamp.strftime('%Y%m%dT%H%M%SZ')}",
67
+ "x-amz-target:ElasticMapReduce.#{@operation}",
68
+ '',
69
+ 'content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target',
70
+ Digest::SHA256.hexdigest(payload)
71
+ ].join("\n")
72
+ end
73
+
74
+ # Task 2: Create a String to Sign for Signature Version 4
75
+ # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
76
+ def string_to_sign
77
+ [
78
+ 'AWS4-HMAC-SHA256',
79
+ @timestamp.strftime('%Y%m%dT%H%M%SZ'),
80
+ credential_scope,
81
+ Digest::SHA256.hexdigest(canonical_request)
82
+ ].join("\n")
83
+ end
84
+
85
+ # Task 3: Calculate the AWS Signature Version 4
86
+ # http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
87
+ def aws_v4_signature
88
+ date = OpenSSL::HMAC.digest('sha256', 'AWS4' + @aws_session.secret_key, @timestamp.strftime('%Y%m%d'))
89
+ region = OpenSSL::HMAC.digest('sha256', date, @aws_session.region)
90
+ service = OpenSSL::HMAC.digest('sha256', region, SERVICE_NAME)
91
+ signing_key = OpenSSL::HMAC.digest('sha256', service, 'aws4_request')
92
+
93
+ OpenSSL::HMAC.hexdigest('sha256', signing_key, string_to_sign)
94
+ end
95
+
96
+ end
97
+
98
+ end
@@ -0,0 +1,72 @@
1
+ module Elasticity
2
+
3
+ class MissingKeyError < StandardError;
4
+ end
5
+ class MissingRegionError < StandardError;
6
+ end
7
+
8
+ class AwsSession
9
+
10
+ attr_reader :access_key
11
+ attr_reader :secret_key
12
+ attr_reader :host
13
+ attr_reader :region
14
+
15
+ # Supported values for options:
16
+ # :region - AWS region (e.g. us-west-1)
17
+ # :secure - true or false, default true.
18
+ def initialize(access=nil, secret=nil, options={})
19
+ # There is a cryptic error if this isn't set
20
+ if options.has_key?(:region) && options[:region] == nil
21
+ raise MissingRegionError, 'A valid :region is required to connect to EMR'
22
+ end
23
+ options[:region] = 'us-east-1' unless options[:region]
24
+ @region = options[:region]
25
+
26
+ @access_key = get_access_key(access)
27
+ @secret_key = get_secret_key(secret)
28
+ @host = "elasticmapreduce.#@region.amazonaws.com"
29
+ end
30
+
31
+ def submit(ruby_service_hash)
32
+ aws_request = AwsRequestV4.new(self, ruby_service_hash)
33
+ begin
34
+ RestClient.post(aws_request.url, aws_request.payload, aws_request.headers)
35
+ rescue RestClient::BadRequest => e
36
+ raise ArgumentError, AwsSession.parse_error_response(e.http_body)
37
+ end
38
+ end
39
+
40
+ def ==(other)
41
+ return false unless other.is_a? AwsSession
42
+ return false unless @access_key == other.access_key
43
+ return false unless @secret_key == other.secret_key
44
+ return false unless @host == other.host
45
+ true
46
+ end
47
+
48
+ private
49
+
50
+ def get_access_key(access)
51
+ return access if access
52
+ return ENV['AWS_ACCESS_KEY_ID'] if ENV['AWS_ACCESS_KEY_ID']
53
+ raise MissingKeyError, 'Please provide an access key or set AWS_ACCESS_KEY_ID.'
54
+ end
55
+
56
+ def get_secret_key(secret)
57
+ return secret if secret
58
+ return ENV['AWS_SECRET_ACCESS_KEY'] if ENV['AWS_SECRET_ACCESS_KEY']
59
+ raise MissingKeyError, 'Please provide a secret key or set AWS_SECRET_ACCESS_KEY.'
60
+ end
61
+
62
+ # AWS error responses all follow the same form. Extract the message from
63
+ # the error document.
64
+ def self.parse_error_response(error_xml)
65
+ xml_doc = Nokogiri::XML(error_xml)
66
+ xml_doc.remove_namespaces!
67
+ xml_doc.xpath('/ErrorResponse/Error/Message').text
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,65 @@
1
+ module Elasticity
2
+
3
+ class AwsUtils
4
+
5
+ # Escape a string according to Amazon's rules.
6
+ # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
7
+ def self.aws_escape(param)
8
+ param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
9
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
10
+ end
11
+ end
12
+
13
+ # With the advent of v4 signing, we can skip the complex translation from v2
14
+ # and ship the JSON over with nearly the same structure.
15
+ def self.convert_ruby_to_aws_v4(value)
16
+ case value
17
+ when Array
18
+ return value.map{|v| convert_ruby_to_aws_v4(v)}
19
+ when Hash
20
+ result = {}
21
+ value.each do |k,v|
22
+ result[camelize(k.to_s)] = convert_ruby_to_aws_v4(v)
23
+ end
24
+ return result
25
+ else
26
+ return value
27
+ end
28
+ end
29
+
30
+ # Since we use the same structure as AWS, we can generate AWS param names
31
+ # from the Ruby versions of those names (and the param nesting).
32
+ def self.convert_ruby_to_aws(params)
33
+ result = {}
34
+ params.each do |key, value|
35
+ case value
36
+ when Array
37
+ prefix = "#{camelize(key.to_s)}.member"
38
+ value.each_with_index do |item, index|
39
+ if item.is_a?(String)
40
+ result["#{prefix}.#{index+1}"] = item
41
+ else
42
+ convert_ruby_to_aws(item).each do |nested_key, nested_value|
43
+ result["#{prefix}.#{index+1}.#{nested_key}"] = nested_value
44
+ end
45
+ end
46
+ end
47
+ when Hash
48
+ prefix = "#{camelize(key.to_s)}"
49
+ convert_ruby_to_aws(value).each do |nested_key, nested_value|
50
+ result["#{prefix}.#{nested_key}"] = nested_value
51
+ end
52
+ else
53
+ result[camelize(key.to_s)] = value
54
+ end
55
+ end
56
+ result
57
+ end
58
+
59
+ def self.camelize(word)
60
+ word.to_s.gsub(/\/(.?)/) { '::' + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -5,7 +5,7 @@ module Elasticity
5
5
  attr_reader :aws_request
6
6
 
7
7
  def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, options = {})
8
- @aws_request = Elasticity::AwsRequest.new(aws_access_key_id, aws_secret_access_key, options)
8
+ @aws_request = Elasticity::AwsSession.new(aws_access_key_id, aws_secret_access_key, options)
9
9
  end
10
10
 
11
11
  # Describe a specific jobflow.
@@ -1,3 +1,3 @@
1
1
  module Elasticity
2
- VERSION = '5.0.1'
2
+ VERSION = '5.0.2'
3
3
  end
@@ -0,0 +1,38 @@
1
+ describe Elasticity::AwsRequestV2 do
2
+
3
+ before do
4
+ Timecop.freeze(Time.at(1302461096))
5
+ end
6
+
7
+ after do
8
+ Timecop.return
9
+ end
10
+
11
+ subject do
12
+ Elasticity::AwsRequestV2.new(
13
+ Elasticity::AwsSession.new('access', 'secret'),
14
+ {:operation => 'RunJobFlow', :name => 'Elasticity Job Flow'}
15
+ )
16
+ end
17
+
18
+ describe '#url' do
19
+ it 'should construct a proper endpoint' do
20
+ subject.url.should == 'https://elasticmapreduce.us-east-1.amazonaws.com'
21
+ end
22
+ end
23
+
24
+ describe '#headers' do
25
+ it 'should create the proper headers' do
26
+ subject.headers.should == {
27
+ :content_type => 'application/x-www-form-urlencoded; charset=utf-8'
28
+ }
29
+ end
30
+ end
31
+
32
+ describe '#payload' do
33
+ it 'should payload up the place' do
34
+ subject.payload.should == 'AWSAccessKeyId=access&Name=Elasticity%20Job%20Flow&Operation=RunJobFlow&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-04-10T18%3A44%3A56.000Z&Signature=5x6YilYHOjgM%2F6nalIOf62txOKoLFGBYyIivoHb%2F27k%3D'
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,80 @@
1
+ describe Elasticity::AwsRequestV4 do
2
+
3
+ before do
4
+ Timecop.freeze(Time.at(1315611360))
5
+ end
6
+
7
+ after do
8
+ Timecop.return
9
+ end
10
+
11
+ subject do
12
+ Elasticity::AwsRequestV4.new(
13
+ Elasticity::AwsSession.new('access', 'secret'),
14
+ {:operation => 'DescribeJobFlows', :job_flow_ids => ['TEST_JOBFLOW_ID']}
15
+ )
16
+ end
17
+
18
+ describe '#url' do
19
+ it 'should construct a proper endpoint' do
20
+ subject.url.should == 'https://elasticmapreduce.us-east-1.amazonaws.com'
21
+ end
22
+ end
23
+
24
+ describe '#headers' do
25
+ it 'should create the proper headers' do
26
+ subject.headers.should == {
27
+ 'Authorization' => "AWS4-HMAC-SHA256 Credential=access/20110909/us-east-1/elasticmapreduce/aws4_request, SignedHeaders=content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=#{subject.send(:aws_v4_signature)}",
28
+ 'Content-Type' => 'application/x-amz-json-1.1',
29
+ 'Host' => 'elasticmapreduce.us-east-1.amazonaws.com',
30
+ 'User-Agent' => "elasticity/#{Elasticity::VERSION}",
31
+ 'X-Amz-Content-SHA256' => Digest::SHA256.hexdigest(subject.payload),
32
+ 'X-Amz-Date' => '20110909T233600Z',
33
+ 'X-Amz-Target' => 'ElasticMapReduce.DescribeJobFlows',
34
+ }
35
+ end
36
+ end
37
+
38
+ describe '#payload' do
39
+ it 'should create the proper payload' do
40
+ subject.payload.should == '{"JobFlowIds":["TEST_JOBFLOW_ID"]}'
41
+ end
42
+ end
43
+
44
+ describe '.canonical_request' do
45
+ it 'should create the proper canonical request' do
46
+ subject.send(:canonical_request).should == [
47
+ 'POST',
48
+ '/',
49
+ '',
50
+ 'content-type:application/x-amz-json-1.1',
51
+ 'host:elasticmapreduce.us-east-1.amazonaws.com',
52
+ "user-agent:elasticity/#{Elasticity::VERSION}",
53
+ "x-amz-content-sha256:#{Digest::SHA256.hexdigest(subject.payload)}",
54
+ 'x-amz-date:20110909T233600Z',
55
+ 'x-amz-target:ElasticMapReduce.DescribeJobFlows',
56
+ '',
57
+ 'content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target',
58
+ "#{Digest::SHA256.hexdigest(subject.payload)}"
59
+ ].join("\n")
60
+ end
61
+ end
62
+
63
+ describe '.string_to_sign' do
64
+ it 'should create the proper string to sign' do
65
+ subject.send(:string_to_sign).should == [
66
+ 'AWS4-HMAC-SHA256',
67
+ '20110909T233600Z',
68
+ '20110909/us-east-1/elasticmapreduce/aws4_request',
69
+ "#{Digest::SHA256.hexdigest(subject.send(:canonical_request))}"
70
+ ].join("\n")
71
+ end
72
+ end
73
+
74
+ describe '.aws_v4_signature' do
75
+ it 'should create the proper signature' do
76
+ subject.send(:aws_v4_signature).should == '3e88b95410e6828f80b4ec476bcf7e23ab8dd380b22ffcb1d5f7e86390346f68'
77
+ end
78
+ end
79
+
80
+ end