elasticity 5.0.1 → 5.0.2

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