123456789_123456789_123456789_123456789_123456789_

Class: Mongo::Auth::Aws::CredentialsRetriever Private

Do not use. This class is for internal use only.
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.

Since:

  • 2.0.0

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Instance Attribute Details

#userAuth::User | nil (readonly)

Returns:

  • (Auth::User | nil)

    The user object, if one was provided.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 67

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.

Parameters:

  • timeout_holder (CsotTimeoutHolder | nil) (defaults to: nil)

    CSOT timeout, if any.

Returns:

Raises:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 82

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_environmentAuth::Aws::Credentials | nil (private)

Returns credentials from environment variables.

Returns:

  • (Auth::Aws::Credentials | nil)

    A set of credentials, or nil if retrieval failed or the obtained credentials are invalid.

Raises:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 123

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.

Parameters:

  • user (Auth::User | nil)

    The user object, if one was provided.

Returns:

Raises:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 105

def credentials_from_user(user)
  return nil unless user

  credentials = Credentials.new(
    user.name,
    user.password,
    user.auth_mech_properties['aws_session_token']
  )
  return 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.

Parameters:

  • response (Net::HTTPResponse)

    AssumeRoleWithWebIdentity call response.

Returns:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 334

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).

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 361

def credentials_valid?(credentials, source)
  unless credentials.access_key_id || credentials.secret_access_key ||
    credentials.session_token
  then
    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.

Parameters:

Returns:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 162

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
  if resp.code != '200'
    return nil
  end
   = resp.body
  resp = with_timeout(timeout_holder) do
    http_get(http, '/latest/meta-data/iam/security-credentials', )
  end
  if resp.code != '200'
    return nil
  end
  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
  if resp.code != '200'
    return nil
  end
  payload = JSON.parse(resp.body)
  unless payload['Code'] == 'Success'
    return nil
  end
  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
  return 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.

Parameters:

Returns:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 214

def (timeout_holder = nil)
  timeout_holder&.check_timeout!
  relative_uri = ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']
  if relative_uri.nil? || relative_uri.empty?
    return nil
  end

  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
  if resp.code != '200'
    return nil
  end
  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
  return nil
end

#http_get(http, uri, metadata_token) (private)

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 350

def http_get(http, uri, )
  req = Net::HTTP::Get.new(uri,
    {'x-aws-ec2-metadata-token' => })
  http.request(req)
end

#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.

Parameters:

Returns:

  • (Auth::Aws::Credentials | nil)

    A set of credentials, or nil if retrieval failed or the obtained credentials are invalid.

Raises:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 143

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_inputsArray<String | nil, String | nil, String | nil> (private)

Returns inputs for the AssumeRoleWithWebIdentity AWS API call.

Returns:

  • (Array<String | nil, String | nil, String | nil>)

    Web identity token, role arn, and role session name.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 269

def prepare_web_identity_inputs
  token_file = ENV['AWS_WEB_IDENTITY_TOKEN_FILE']
  role_arn = ENV['AWS_ROLE_ARN']
  if token_file.nil? || role_arn.nil?
    return nil
  end
  web_identity_token = File.open(token_file).read
  role_session_name = ENV['AWS_ROLE_SESSION_NAME']
  if role_session_name.nil?
    role_session_name = "ruby-app-#{SecureRandom.alphanumeric(50)}"
  end
  [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.

Parameters:

  • token (String)

    The OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.

  • role_arn (String)

    The Amazon Resource Name (ARN) of the role that the caller is assuming.

  • role_session_name (String)

    An identifier for the assumed role session.

  • timeout_holder (CsotTimeoutHolder | nil)

    CSOT timeout.

Returns:

  • (Net::HTTPResponse | nil)

    AWS API response if successful, otherwise nil.

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 301

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
  if resp.code != '200'
    return nil
  end
  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.

Parameters:

Returns:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 255

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) (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.

Parameters:

Since:

  • 2.0.0

[ GitHub ]

  
# File 'lib/mongo/auth/aws/credentials_retriever.rb', line 396

def with_timeout(timeout_holder)
  timeout = timeout_holder&.remaining_timeout_sec! || METADATA_TIMEOUT
  exception_class = if timeout_holder&.csot?
                      Error::TimeoutError
                    else
                      nil
                    end
  ::Timeout.timeout(timeout, exception_class) do
    yield
  end
end