Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalized ECS provider v2 #2882

Merged
merged 12 commits into from
Nov 20, 2023
2 changes: 2 additions & 0 deletions gems/aws-sdk-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Feature - Support `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` in `ECSCredentials` and allow for link-local http addresses.

* Feature - Add support for configuring the endpoint URL in the shared configuration file or via an environment variable for a specific AWS service or all AWS services.

3.177.0 (2023-07-06)
Expand Down
62 changes: 56 additions & 6 deletions gems/aws-sdk-core/lib/aws-sdk-core/ecs_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class ECSCredentials
# @api private
class Non200Response < RuntimeError; end

# Raised when the token file cannot be read.
class TokenFileReadError < RuntimeError; end

# These are the errors we trap when attempting to talk to the
# instance metadata service. Any of these imply the service
# is not present, no responding or some other non-recoverable
Expand Down Expand Up @@ -64,7 +67,6 @@ def initialize(options = {})
endpoint = options[:endpoint] ||
ENV['AWS_CONTAINER_CREDENTIALS_FULL_URI']
initialize_uri(options, credential_path, endpoint)
@authorization_token = ENV['AWS_CONTAINER_AUTHORIZATION_TOKEN']

@retries = options[:retries] || 5
@http_open_timeout = options[:http_open_timeout] || 5
Expand Down Expand Up @@ -103,31 +105,43 @@ def initialize_relative_uri(options, path)

def initialize_full_uri(endpoint)
uri = URI.parse(endpoint)
validate_full_uri_scheme!(uri)
validate_full_uri!(uri)
@host = uri.host
@port = uri.port
@scheme = uri.scheme
@credential_path = uri.path
@credential_path = uri.request_uri
end

def validate_full_uri_scheme!(full_uri)
return if full_uri.is_a?(URI::HTTP) || full_uri.is_a?(URI::HTTPS)

raise ArgumentError, "'#{full_uri}' must be a valid HTTP or HTTPS URI"
end

# Validate that the full URI is using a loopback address if scheme is http.
def validate_full_uri!(full_uri)
return unless full_uri.scheme == 'http'

begin
return if ip_loopback?(IPAddr.new(full_uri.host))
return if valid_ip_address?(IPAddr.new(full_uri.host))
rescue IPAddr::InvalidAddressError
addresses = Resolv.getaddresses(full_uri.host)
return if addresses.all? { |addr| ip_loopback?(IPAddr.new(addr)) }
return if addresses.all? { |addr| valid_ip_address?(IPAddr.new(addr)) }
end

raise ArgumentError,
'AWS_CONTAINER_CREDENTIALS_FULL_URI must use a loopback '\
'address when using the http scheme.'
'or a link-local address when using the http scheme.'
end

def valid_ip_address?(ip_address)
ip_loopback?(ip_address) || ip_link_local?(ip_address)
end

# loopback? method is available in Ruby 2.5+
# Replicate the logic here.
# loopback (IPv4 127.0.0.0/8, IPv6 ::1/128)
def ip_loopback?(ip_address)
case ip_address.family
when Socket::AF_INET
Expand All @@ -139,6 +153,21 @@ def ip_loopback?(ip_address)
end
end

# link_local? method is available in Ruby 2.5+
# Replicate the logic here.
# link-local (IPv4 169.254.0.0/16, IPv6 fe80::/10)
def ip_link_local?(ip_address)
case ip_address.family
when Socket::AF_INET
ip_address & 0xffff0000 == 0xa9fe0000
when Socket::AF_INET6
ip_address & 0xffc0_0000_0000_0000_0000_0000_0000_0000 ==
0xfe80_0000_0000_0000_0000_0000_0000_0000
else
false
end
end

def backoff(backoff)
case backoff
when Proc then backoff
Expand Down Expand Up @@ -174,10 +203,28 @@ def get_credentials
http_get(conn, @credential_path)
end
end
rescue TokenFileReadError
raise
rescue StandardError
'{}'
end

def fetch_authorization_token
if (path = ENV['AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE'])
mullermp marked this conversation as resolved.
Show resolved Hide resolved
fetch_authorization_token_file(path)
elsif (token = ENV['AWS_CONTAINER_AUTHORIZATION_TOKEN'])
token
end
end

def fetch_authorization_token_file(path)
File.read(path).strip
rescue Errno::ENOENT
raise TokenFileReadError,
'AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE is set '\
"but the file doesn't exist: #{path}"
end

def open_connection
http = Net::HTTP.new(@host, @port, nil)
http.open_timeout = @http_open_timeout
Expand All @@ -190,7 +237,8 @@ def open_connection

def http_get(connection, path)
request = Net::HTTP::Get.new(path)
request['Authorization'] = @authorization_token if @authorization_token
authorization_token = fetch_authorization_token
request['Authorization'] = authorization_token if authorization_token
response = connection.request(request)
raise Non200Response unless response.code.to_i == 200

Expand All @@ -202,6 +250,8 @@ def retry_errors(error_classes, options = {})
retries = 0
begin
yield
rescue TokenFileReadError
mullermp marked this conversation as resolved.
Show resolved Hide resolved
raise
rescue *error_classes => _e
raise unless retries < max_retries

Expand Down
176 changes: 176 additions & 0 deletions gems/aws-sdk-core/spec/aws/ecs_credentials_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
[
{
"description": "should reject malformed full URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "192.168.1.1"
},
"expect": {
"type": "error",
"reason": "'192.168.1.1' must be a valid HTTP or HTTPS URI"
}
},
{
"description": "should reject forbidden host in full URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://192.168.1.1/endpoint"
},
"expect": {
"type": "error",
"reason": "'192.168.1.1' is not an allowed host"
}
},
{
"description": "should reject invalid token file path",
"env": {
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/endpoint",
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE": "/full/path/to/token/file",
"AWS_CONTAINER_AUTHORIZATION_TOKEN": "footoken2"
},
"token_file": {
"type": "error",
"errno": "ENOENT"
},
"expect": {
"type": "error",
"reason": "failed to read authorization token from '/full/path/to/token/file': no such file or directory"
}
},
{
"description": "https URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "https://awscredentials.amazonaws.com/credentials"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "https://awscredentials.amazonaws.com/credentials",
"headers": {}
}
}
},
{
"description": "http loopback(v4) URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://127.0.0.2/credentials"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "http://127.0.0.2/credentials",
"headers": {}
}
}
},
{
"description": "http link-local(v4) URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://169.254.0.2/credentials"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "http://169.254.0.2/credentials",
"headers": {}
}
}
},
{
"description": "http loopback(v6) URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://[::1]/credentials"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "http://[::1]/credentials",
"headers": {}
}
}
},
{
"description": "http link-local(v6) URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://[fe80::2]/credentials"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "http://[fe80::2]/credentials",
"headers": {}
}
}
},
{
"description": "complex full URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://127.0.0.1:8080/credentials?foo=bar%20baz"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "http://127.0.0.1:8080/credentials?foo=bar%20baz",
"headers": {}
}
}
},
{
"description": "relative URI, ignore full URI",
"env": {
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/credentials-relative?foo=bar%20baz",
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "http://127.0.0.1:8080/credentials?foo=bar%20baz"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "http://169.254.170.2/credentials-relative?foo=bar%20baz",
"headers": {}
}
}
},
{
"description": "auth token from file",
"env": {
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/credentials-relative",
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE": "/path/to/token",
"AWS_CONTAINER_AUTHORIZATION_TOKEN": "static%20token2"
},
"token_file": {
"type": "success",
"content": "static%20token"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "http://169.254.170.2/credentials-relative",
"headers": {
"Authorization": "static%20token"
}
}
}
},
{
"description": "auth token from env",
"env": {
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/credentials-relative",
"AWS_CONTAINER_AUTHORIZATION_TOKEN": "static%20token2"
},
"expect": {
"type": "success",
"request": {
"method": "GET",
"uri": "http://169.254.170.2/credentials-relative",
"headers": {
"Authorization": "static%20token2"
}
}
}
}
]
Loading