Class: Mongo::Auth::Aws::CredentialsRetriever Private
| Relationships & Source Files | |
| Inherits: | Object |
| Defined in: | lib/mongo/auth/aws/credentials_retriever.rb |
Overview
Retrieves AWS credentials from a variety of sources.
This class provides for AWS credentials retrieval from:
-
the passed user (which receives the credentials passed to the client via URI options and Ruby options)
-
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN environment variables (commonly used by AWS SDKs and various tools, as well as AWS Lambda)
-
AssumeRoleWithWebIdentity API call
-
EC2 metadata endpoint
-
ECS metadata endpoint
The sources listed above are consulted in the order specified. The first source that contains any of the three credential components (access key id, secret access key or session token) is used. The credential components must form a valid set if any of the components is specified; meaning, access key id and secret access key must always be provided together, and if a session token is provided the key id and secret key must also be provided. If a source provides partial credentials, credential retrieval fails with an exception.
Constant Summary
-
METADATA_TIMEOUT =
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 56
::Mongo::Timeoutfor metadata operations, in seconds.The auth spec suggests a 10 second timeout but this seems excessively long given that the endpoint is essentially local.
5
Class Method Summary
- .new(user = nil, credentials_cache: CredentialsCache.instance) ⇒ CredentialsRetriever constructor Internal use only
Instance Attribute Summary
- #user ⇒ Auth::User | nil readonly Internal use only
Instance Method Summary
-
#credentials(timeout_holder = nil) ⇒ Auth::Aws::Credentials
Internal use only
Retrieves a valid set of credentials, if possible, or raises
::Mongo::Auth::InvalidConfiguration. -
#credentials_from_environment ⇒ Auth::Aws::Credentials | nil
private
Internal use only
Returns credentials from environment variables.
-
#credentials_from_user(user) ⇒ Auth::Aws::Credentials | nil
private
Internal use only
Returns credentials from the user object.
-
#credentials_from_web_identity_response(response) ⇒ Auth::Aws::Credentials | nil
private
Internal use only
Extracts credentials from AssumeRoleWithWebIdentity response.
-
#credentials_valid?(credentials, source) ⇒ Boolean
private
Internal use only
Checks whether the credentials provided are valid.
-
#ec2_metadata_credentials(timeout_holder = nil) ⇒ Auth::Aws::Credentials | nil
private
Internal use only
Returns credentials from the EC2 metadata endpoint.
-
#ecs_metadata_credentials(timeout_holder = nil) ⇒ Auth::Aws::Credentials | nil
private
Internal use only
Returns credentials from the ECS metadata endpoint.
- #http_get(http, uri, metadata_token) private Internal use only
-
#obtain_credentials_from_endpoints(timeout_holder = nil) ⇒ Auth::Aws::Credentials | nil
private
Internal use only
Returns credentials from the AWS metadata endpoints.
-
#prepare_web_identity_inputs ⇒ Array<String | nil, String | nil, String | nil>
private
Internal use only
Returns inputs for the AssumeRoleWithWebIdentity AWS API call.
-
#request_web_identity_credentials(token, role_arn, role_session_name, timeout_holder) ⇒ Net::HTTPResponse | nil
private
Internal use only
Calls AssumeRoleWithWebIdentity to obtain credentials for the given web identity token.
-
#web_identity_credentials(timeout_holder = nil) ⇒ Auth::Aws::Credentials | nil
private
Internal use only
Returns credentials associated with web identity token that is stored in a file.
-
#with_timeout(timeout_holder, &block)
private
Internal use only
Execute the given block considering the timeout defined on the context, or the default timeout value.
Instance Attribute Details
#user ⇒ Auth::User | nil (readonly)
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 66
attr_reader :user
Instance Method Details
#credentials(timeout_holder = nil) ⇒ Auth::Aws::Credentials
Retrieves a valid set of credentials, if possible, or raises ::Mongo::Auth::InvalidConfiguration.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 81
def credentials(timeout_holder = nil) credentials = credentials_from_user(user) return credentials unless credentials.nil? credentials = credentials_from_environment return credentials unless credentials.nil? credentials = @credentials_cache.fetch { obtain_credentials_from_endpoints(timeout_holder) } return credentials unless credentials.nil? raise Auth::Aws::CredentialsNotFound end
#credentials_from_environment ⇒ Auth::Aws::Credentials | nil (private)
Returns credentials from environment variables.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 122
def credentials_from_environment credentials = Credentials.new( ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'], ENV['AWS_SESSION_TOKEN'] ) credentials if credentials && credentials_valid?(credentials, 'environment variables') end
#credentials_from_user(user) ⇒ Auth::Aws::Credentials | nil (private)
Returns credentials from the user object.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 104
def credentials_from_user(user) return nil unless user credentials = Credentials.new( user.name, user.password, user.auth_mech_properties['aws_session_token'] ) credentials if credentials_valid?(credentials, 'Mongo::Client URI or Ruby options') end
#credentials_from_web_identity_response(response) ⇒ Auth::Aws::Credentials | nil (private)
Extracts credentials from AssumeRoleWithWebIdentity response.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 327
def credentials_from_web_identity_response(response) payload = JSON.parse(response.body).dig( 'AssumeRoleWithWebIdentityResponse', 'AssumeRoleWithWebIdentityResult', 'Credentials' ) || {} Credentials.new( payload['AccessKeyId'], payload['SecretAccessKey'], payload['SessionToken'], Time.at(payload['Expiration']) ) rescue JSON::ParserError, TypeError nil end
#credentials_valid?(credentials, source) ⇒ Boolean (private)
Checks whether the credentials provided are valid.
Returns true if they are valid, false if they are empty, and raises ::Mongo::Auth::InvalidConfiguration if the credentials are incomplete (i.e. some of the components are missing).
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 354
def credentials_valid?(credentials, source) unless credentials.access_key_id || credentials.secret_access_key || credentials.session_token return false end if credentials.access_key_id || credentials.secret_access_key if credentials.access_key_id && !credentials.secret_access_key raise Auth::InvalidConfiguration, "Access key ID is provided without secret access key (source: #{source})" end if credentials.secret_access_key && !credentials.access_key_id raise Auth::InvalidConfiguration, "Secret access key is provided without access key ID (source: #{source})" end elsif credentials.session_token raise Auth::InvalidConfiguration, "Session token is provided without access key ID or secret access key (source: #{source})" end true end
#ec2_metadata_credentials(timeout_holder = nil) ⇒ Auth::Aws::Credentials | nil (private)
Returns credentials from the EC2 metadata endpoint. The credentials could be empty, partial or invalid.
@ raise ::Mongo::Error::TimeoutError if credentials cannot be retrieved within
the timeout.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 164
def (timeout_holder = nil) timeout_holder&.check_timeout! http = Net::HTTP.new('169.254.169.254') req = Net::HTTP::Put.new('/latest/api/token', # The TTL is required in order to obtain the metadata token. { 'x-aws-ec2-metadata-token-ttl-seconds' => '30' }) resp = with_timeout(timeout_holder) do http.request(req) end return nil if resp.code != '200' = resp.body resp = with_timeout(timeout_holder) do http_get(http, '/latest/meta-data/iam/security-credentials', ) end return nil if resp.code != '200' role_name = resp.body escaped_role_name = CGI.escape(role_name).gsub('+', '%20') resp = with_timeout(timeout_holder) do http_get(http, "/latest/meta-data/iam/security-credentials/#{escaped_role_name}", ) end return nil if resp.code != '200' payload = JSON.parse(resp.body) return nil unless payload['Code'] == 'Success' Credentials.new( payload['AccessKeyId'], payload['SecretAccessKey'], payload['Token'], DateTime.parse(payload['Expiration']).to_time ) # When trying to use the EC2 metadata endpoint on ECS: # Errno::EINVAL: Failed to open TCP connection to 169.254.169.254:80 (Invalid argument - connect(2) for "169.254.169.254" port 80) rescue ::Timeout::Error, IOError, SystemCallError, TypeError nil end
#ecs_metadata_credentials(timeout_holder = nil) ⇒ Auth::Aws::Credentials | nil (private)
Returns credentials from the ECS metadata endpoint. The credentials could be empty, partial or invalid.
@ raise ::Mongo::Error::TimeoutError if credentials cannot be retrieved within
the timeout defined on the operation context.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 212
def (timeout_holder = nil) timeout_holder&.check_timeout! relative_uri = ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] return nil if relative_uri.nil? || relative_uri.empty? http = Net::HTTP.new('169.254.170.2') # Per https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html # the value in AWS_CONTAINER_CREDENTIALS_RELATIVE_URI includes # the leading slash. # The current language in MONGODB-AWS specification implies that # a leading slash must be added by the driver, but this is not # in fact needed. req = Net::HTTP::Get.new(relative_uri) resp = with_timeout(timeout_holder) do http.request(req) end return nil if resp.code != '200' payload = JSON.parse(resp.body) Credentials.new( payload['AccessKeyId'], payload['SecretAccessKey'], payload['Token'], DateTime.parse(payload['Expiration']).to_time ) rescue ::Timeout::Error, IOError, SystemCallError, TypeError nil end
#http_get(http, uri, metadata_token) (private)
#obtain_credentials_from_endpoints(timeout_holder = nil) ⇒ Auth::Aws::Credentials | nil (private)
Returns credentials from the AWS metadata endpoints.
@ raise ::Mongo::Error::TimeoutError if credentials cannot be retrieved within
the timeout defined on the operation context.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 142
def obtain_credentials_from_endpoints(timeout_holder = nil) if (credentials = web_identity_credentials(timeout_holder)) && credentials_valid?(credentials, 'Web identity token') credentials elsif (credentials = (timeout_holder)) && credentials_valid?(credentials, 'ECS task metadata') credentials elsif (credentials = (timeout_holder)) && credentials_valid?(credentials, 'EC2 instance metadata') credentials end end
#prepare_web_identity_inputs ⇒ Array<String | nil, String | nil, String | nil> (private)
Returns inputs for the AssumeRoleWithWebIdentity AWS API call.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 266
def prepare_web_identity_inputs token_file = ENV['AWS_WEB_IDENTITY_TOKEN_FILE'] role_arn = ENV['AWS_ROLE_ARN'] return nil if token_file.nil? || role_arn.nil? web_identity_token = File.read(token_file) role_session_name = ENV['AWS_ROLE_SESSION_NAME'] role_session_name = "ruby-app-#{SecureRandom.alphanumeric(50)}" if role_session_name.nil? [ web_identity_token, role_arn, role_session_name ] rescue Errno::ENOENT, IOError, SystemCallError nil end
#request_web_identity_credentials(token, role_arn, role_session_name, timeout_holder) ⇒ Net::HTTPResponse | nil (private)
Calls AssumeRoleWithWebIdentity to obtain credentials for the given web identity token.
@ raise ::Mongo::Error::TimeoutError if credentials cannot be retrieved within
the timeout defined on the operation context.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 295
def request_web_identity_credentials(token, role_arn, role_session_name, timeout_holder) timeout_holder&.check_timeout! uri = URI('https://sts.amazonaws.com/') params = { 'Action' => 'AssumeRoleWithWebIdentity', 'Version' => '2011-06-15', 'RoleArn' => role_arn, 'WebIdentityToken' => token, 'RoleSessionName' => role_session_name } uri.query = ::URI.encode_www_form(params) req = Net::HTTP::Post.new(uri) req['Accept'] = 'application/json' resp = with_timeout(timeout_holder) do Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |https| https.request(req) end end return nil if resp.code != '200' resp rescue Errno::ENOENT, IOError, SystemCallError nil end
#web_identity_credentials(timeout_holder = nil) ⇒ Auth::Aws::Credentials | nil (private)
Returns credentials associated with web identity token that is stored in a file. This authentication mechanism is used to authenticate inside EKS. See docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html for further details.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 250
def web_identity_credentials(timeout_holder = nil) web_identity_token, role_arn, role_session_name = prepare_web_identity_inputs return nil if web_identity_token.nil? response = request_web_identity_credentials( web_identity_token, role_arn, role_session_name, timeout_holder ) return if response.nil? credentials_from_web_identity_response(response) end
#with_timeout(timeout_holder, &block) (private)
Execute the given block considering the timeout defined on the context, or the default timeout value.
We use Timeout.timeout here because there is no other acceptable easy way to time limit http requests.
@ raise ::Mongo::Error::TimeoutError if deadline exceeded.
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 388
def with_timeout(timeout_holder, &block) timeout = timeout_holder&.remaining_timeout_sec! || METADATA_TIMEOUT exception_class = (Error::TimeoutError if timeout_holder&.csot?) ::Timeout.timeout(timeout, exception_class, &block) end