class Google::Auth::ExternalAccount::PluggableAuthCredentials

This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI provider then exchanging the credentials for a short-lived Google Cloud access token.

Constants

ENABLE_PLUGGABLE_ENV

constant for pluggable auth enablement in environment variable.

EXECUTABLE_SUPPORTED_MAX_VERSION
EXECUTABLE_TIMEOUT_MILLIS_DEFAULT
EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND
EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND
ID_TOKEN_TYPE

Attributes

client_id[R]

Will always be nil, but method still gets used.

Public Class Methods

new(options = {}) click to toggle source

Initialize from options map.

@param [string] audience @param [hash{symbol => value}] credential_source

credential_source is a hash that contains either source file or url.
credential_source_format is either text or json. To define how we parse the credential response.
# File lib/googleauth/external_account/pluggable_credentials.rb, line 49
def initialize options = {}
  base_setup options

  @audience = options[:audience]
  @credential_source = options[:credential_source] || {}
  @credential_source_executable = @credential_source[:executable]
  raise "Missing excutable source. An 'executable' must be provided" if @credential_source_executable.nil?
  @credential_source_executable_command = @credential_source_executable[:command]
  if @credential_source_executable_command.nil?
    raise "Missing command field. Executable command must be provided."
  end
  @credential_source_executable_timeout_millis = @credential_source_executable[:timeout_millis] ||
                                                 EXECUTABLE_TIMEOUT_MILLIS_DEFAULT
  if @credential_source_executable_timeout_millis < EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND ||
     @credential_source_executable_timeout_millis > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND
    raise "Timeout must be between 5 and 120 seconds."
  end
  @credential_source_executable_output_file = @credential_source_executable[:output_file]
end

Public Instance Methods

retrieve_subject_token!() click to toggle source
# File lib/googleauth/external_account/pluggable_credentials.rb, line 69
def retrieve_subject_token!
  unless ENV[ENABLE_PLUGGABLE_ENV] == "1"
    raise "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') " \
          "to run."
  end
  # check output file first
  subject_token = load_subject_token_from_output_file
  return subject_token unless subject_token.nil?
  # environment variable injection
  env = inject_environment_variables
  output = subprocess_with_timeout env, @credential_source_executable_command,
                                   @credential_source_executable_timeout_millis
  response = MultiJson.load output, symbolize_keys: true
  parse_subject_token response
end

Private Instance Methods

inject_environment_variables() click to toggle source
# File lib/googleauth/external_account/pluggable_credentials.rb, line 130
def inject_environment_variables
  env = ENV.to_h
  env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = @audience
  env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = @subject_token_type
  env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0" # only non-interactive mode we support.
  unless @service_account_impersonation_url.nil?
    env["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = service_account_email
  end
  unless @credential_source_executable_output_file.nil?
    env["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = @credential_source_executable_output_file
  end
  env
end
load_subject_token_from_output_file() click to toggle source
# File lib/googleauth/external_account/pluggable_credentials.rb, line 87
def load_subject_token_from_output_file
  return nil if @credential_source_executable_output_file.nil?
  return nil unless File.exist? @credential_source_executable_output_file
  begin
    content = File.read @credential_source_executable_output_file, encoding: "utf-8"
    response = MultiJson.load content, symbolize_keys: true
  rescue StandardError
    return nil
  end
  begin
    subject_token = parse_subject_token response
  rescue StandardError => e
    return nil if e.message.match(/The token returned by the executable is expired/)
    raise e
  end
  subject_token
end
parse_subject_token(response) click to toggle source
# File lib/googleauth/external_account/pluggable_credentials.rb, line 105
def parse_subject_token response
  validate_response_schema response
  unless response[:success]
    if response[:code].nil? || response[:message].nil?
      raise "Error code and message fields are required in the response."
    end
    raise "Executable returned unsuccessful response: code: #{response[:code]}, message: #{response[:message]}."
  end
  if response[:expiration_time] && response[:expiration_time] < Time.now.to_i
    raise "The token returned by the executable is expired."
  end
  raise "The executable response is missing the token_type field." if response[:token_type].nil?
  return response[:id_token] if ID_TOKEN_TYPE.include? response[:token_type]
  return response[:saml_response] if response[:token_type] == "urn:ietf:params:oauth:token-type:saml2"
  raise "Executable returned unsupported token type."
end
subprocess_with_timeout(environment_vars, command, timeout_seconds) click to toggle source
# File lib/googleauth/external_account/pluggable_credentials.rb, line 144
def subprocess_with_timeout environment_vars, command, timeout_seconds
  Timeout.timeout timeout_seconds do
    output, error, status = Open3.capture3 environment_vars, command
    unless status.success?
      raise "Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}"
    end
    output
  end
end
validate_response_schema(response) click to toggle source
# File lib/googleauth/external_account/pluggable_credentials.rb, line 122
def validate_response_schema response
  raise "The executable response is missing the version field." if response[:version].nil?
  if response[:version] > EXECUTABLE_SUPPORTED_MAX_VERSION
    raise "Executable returned unsupported version #{response[:version]}."
  end
  raise "The executable response is missing the success field." if response[:success].nil?
end