Skip to content

Commit 01ae9e4

Browse files
pjsk-stripehsbt
authored andcommitted
[rubygems/rubygems] implement fallback
rubygems/rubygems@e09a6ec815
1 parent 23b3451 commit 01ae9e4

File tree

2 files changed

+75
-36
lines changed

2 files changed

+75
-36
lines changed

lib/rubygems/s3_uri_signer.rb

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# frozen_string_literal: true
22

33
require_relative "openssl"
4+
require_relative "user_interaction"
45

56
##
67
# S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems
78
# More on AWS SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
89
class Gem::S3URISigner
10+
include Gem::UserInteraction
11+
912
class ConfigurationError < Gem::Exception
1013
def initialize(message)
1114
super message
@@ -146,19 +149,40 @@ def ec2_metadata_credentials_json
146149
require_relative "request"
147150
require_relative "request/connection_pools"
148151
require "json"
152+
153+
# First try V2 fallback to V1
154+
res = nil
155+
begin
156+
res = ec2_metadata_credentials_imds_v2
157+
rescue InstanceProfileError
158+
alert_warning "Unable to access ec2 credentials via IMDSv2, falling back to IMDSv1"
159+
res = ec2_metadata_credentials_imds_v1
160+
end
161+
res
162+
end
163+
164+
def ec2_metadata_credentials_imds_v2
149165
token = ec2_metadata_token
166+
iam_info = ec2_metadata_request(EC2_IAM_INFO, token:)
167+
# Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
168+
role_name = iam_info["InstanceProfileArn"].split("/").last
169+
ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token:)
170+
end
150171

151-
iam_info = ec2_metadata_request(EC2_IAM_INFO, token)
172+
def ec2_metadata_credentials_imds_v1
173+
iam_info = ec2_metadata_request(EC2_IAM_INFO, token: nil)
152174
# Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
153175
role_name = iam_info["InstanceProfileArn"].split("/").last
154-
ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token)
176+
ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token: nil)
155177
end
156178

157-
def ec2_metadata_request(url, token)
179+
def ec2_metadata_request(url, token:)
158180
request = ec2_iam_request(Gem::URI(url), Gem::Net::HTTP::Get)
159181

160182
response = request.fetch do |req|
161-
req.add_field "X-aws-ec2-metadata-token", token
183+
if token
184+
req.add_field "X-aws-ec2-metadata-token", token
185+
end
162186
end
163187

164188
case response
@@ -185,11 +209,8 @@ def ec2_metadata_token
185209
end
186210

187211
def ec2_iam_request(uri, verb)
188-
@request_pool ||= {}
189-
@request_pool[uri] ||= create_request_pool(uri)
190-
pool = @request_pool[uri]
191-
192-
Gem::Request.new(uri, verb, nil, pool)
212+
@request_pool ||= create_request_pool(uri)
213+
Gem::Request.new(uri, verb, nil, @request_pool)
193214
end
194215

195216
def create_request_pool(uri)

test/rubygems/test_gem_remote_fetcher_s3.rb

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ class TestGemRemoteFetcherS3 < Gem::TestCase
99
include Gem::DefaultUserInteraction
1010

1111
class FakeGemRequest < Gem::Request
12-
1312
attr_reader :last_request, :uri
1413

1514
# Override perform_request to stub things
@@ -52,12 +51,13 @@ def ec2_iam_request(uri, verb)
5251

5352
case uri.to_s
5453
when "http://169.254.169.254/latest/api/token"
55-
res = Gem::Net::HTTPOK.new nil, 200, nil
56-
def res.body
57-
"mysecrettoken"
54+
if $imdsv2_token_failure
55+
res = Gem::Net::HTTPUnauthorized.new nil, 401, nil
56+
def res.body = "you got a 401! panic!"
57+
else
58+
res = Gem::Net::HTTPOK.new nil, 200, nil
59+
def res.body = "mysecrettoken"
5860
end
59-
fake_s3_request.set_response(res)
60-
6161
when "http://169.254.169.254/latest/meta-data/iam/info"
6262
res = Gem::Net::HTTPOK.new nil, 200, nil
6363
def res.body
@@ -70,33 +70,26 @@ def res.body
7070
}
7171
JSON
7272
end
73-
fake_s3_request.set_response(res)
7473

7574
when "http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole"
7675
res = Gem::Net::HTTPOK.new nil, 200, nil
77-
def res.body
78-
$instance_profile
79-
end
80-
fake_s3_request.set_response(res)
81-
76+
def res.body = $instance_profile
8277
else
8378
raise "Unexpected request to #{uri}"
8479
end
8580

81+
fake_s3_request.set_response(res)
8682
fake_s3_request
8783
end
8884
end
8985

9086
class FakeGemFetcher < Gem::RemoteFetcher
91-
9287
attr_reader :fetched_uri, :last_s3_uri_signer
9388

9489
def request(uri, request_class, last_modified = nil)
9590
@fetched_uri = uri
9691
res = Gem::Net::HTTPOK.new nil, 200, nil
97-
def res.body
98-
"success"
99-
end
92+
def res.body = "success"
10093
res
10194
end
10295

@@ -132,10 +125,33 @@ def assert_fetched_s3_with_imds_v2
132125
assert_equal(expected.strip, recent_aws_query_logs.strip)
133126
end
134127

135-
def assert_fetch_s3(url:, signature:, token: nil, region: "us-east-1", instance_profile_json: nil, method: "GET")
136-
@fetcher = FakeGemFetcher.new nil
128+
def assert_fetched_s3_with_imds_v1
129+
# Three API requests:
130+
# 1. Get the token (which fails)
131+
# 2. Lookup profile details without token
132+
# 3. Query the credentials without token
133+
expected = <<~TEXT
134+
PUT http://169.254.169.254/latest/api/token
135+
x-aws-ec2-metadata-token-ttl-seconds=60
136+
GET http://169.254.169.254/latest/meta-data/iam/info
137+
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole
138+
TEXT
139+
recent_aws_query_logs = @fetcher.last_s3_uri_signer.recent_aws_query_logs
140+
assert_equal(expected.strip, recent_aws_query_logs.strip)
141+
end
142+
143+
def with_imds_v2_failure
144+
$imdsv2_token_failure = true
145+
yield(fetcher)
146+
ensure
147+
$imdsv2_token_failure = nil
148+
end
149+
150+
def assert_fetch_s3(url:, signature:, token: nil, region: "us-east-1", instance_profile_json: nil, fetcher: nil, method: "GET")
151+
@fetcher = fetcher || FakeGemFetcher.new(nil)
137152
$instance_profile = instance_profile_json
138-
res = fetcher.fetch_s3 Gem::URI.parse(url), nil, (method == "HEAD")
153+
res = @fetcher.fetch_s3 Gem::URI.parse(url), nil, (method == "HEAD")
154+
$imdsv2_token_failure ||= nil
139155

140156
assert_equal "https://my-bucket.s3.#{region}.amazonaws.com/gems/specs.4.8.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=testuser%2F20190624%2F#{region}%2Fs3%2Faws4_request&X-Amz-Date=20190624T051941Z&X-Amz-Expires=86400#{token ? "&X-Amz-Security-Token=" + token : ""}&X-Amz-SignedHeaders=host&X-Amz-Signature=#{signature}", @fetcher.fetched_uri.to_s
141157
if method == "HEAD"
@@ -373,14 +389,16 @@ def test_fetch_s3_instance_profile_creds_with_fallback
373389

374390
url = "s3://my-bucket/gems/specs.4.8.gz"
375391
Time.stub :now, Time.at(1_561_353_581) do
376-
assert_fetch_s3(
377-
url: url,
378-
signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625",
379-
token: "testtoken",
380-
region: "us-east-1",
381-
instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}'
382-
)
383-
assert_fetched_s3_with_imds_v2
392+
with_imds_v2_failure do
393+
assert_fetch_s3(
394+
url: url,
395+
signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625",
396+
token: "testtoken",
397+
region: "us-east-1",
398+
instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}'
399+
)
400+
assert_fetched_s3_with_imds_v1
401+
end
384402
end
385403
ensure
386404
Gem.configuration[:s3_source] = nil

0 commit comments

Comments
 (0)