1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
|
# Copyright 2023 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "open3"
require "time"
require "googleauth/errors"
require "googleauth/external_account/base_credentials"
require "googleauth/external_account/external_account_utils"
module Google
# Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
module Auth
module ExternalAccount
# 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.
class PluggableAuthCredentials
# constant for pluggable auth enablement in environment variable.
ENABLE_PLUGGABLE_ENV = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES".freeze
EXECUTABLE_SUPPORTED_MAX_VERSION = 1
EXECUTABLE_TIMEOUT_MILLIS_DEFAULT = 30 * 1000
EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 1000
EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND = 120 * 1000
ID_TOKEN_TYPE = ["urn:ietf:params:oauth:token-type:jwt", "urn:ietf:params:oauth:token-type:id_token"].freeze
include Google::Auth::ExternalAccount::BaseCredentials
include Google::Auth::ExternalAccount::ExternalAccountUtils
extend CredentialsLoader
# Will always be nil, but method still gets used.
attr_reader :client_id
# Initialize from options map.
#
# @param [Hash] options Configuration options
# @option options [String] :audience Audience for the token
# @option options [Hash] :credential_source Credential source configuration that contains executable
# configuration
# @raise [Google::Auth::InitializationError] If executable source, command is missing, or timeout is invalid
def initialize options = {}
base_setup options
@audience = options[:audience]
@credential_source = options[:credential_source] || {}
@credential_source_executable = @credential_source[:executable]
if @credential_source_executable.nil?
raise InitializationError,
"Missing excutable source. An 'executable' must be provided"
end
@credential_source_executable_command = @credential_source_executable[:command]
if @credential_source_executable_command.nil?
raise InitializationError, "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 InitializationError, "Timeout must be between 5 and 120 seconds."
end
@credential_source_executable_output_file = @credential_source_executable[:output_file]
end
# Retrieves the subject token using the credential_source object.
#
# @return [String] The retrieved subject token
# @raise [Google::Auth::CredentialsError] If executables are not allowed, if token retrieval fails,
# or if the token is invalid
def retrieve_subject_token!
unless ENV[ENABLE_PLUGGABLE_ENV] == "1"
raise CredentialsError,
"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
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 CredentialsError, e.message
end
subject_token
end
def parse_subject_token response
validate_response_schema response
unless response[:success]
if response[:code].nil? || response[:message].nil?
raise CredentialsError, "Error code and message fields are required in the response."
end
raise CredentialsError,
"Executable returned unsuccessful response: code: #{response[:code]}, message: #{response[:message]}."
end
if response[:expiration_time] && response[:expiration_time] < Time.now.to_i
raise CredentialsError, "The token returned by the executable is expired."
end
if response[:token_type].nil?
raise CredentialsError,
"The executable response is missing the token_type field."
end
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 CredentialsError, "Executable returned unsupported token type."
end
def validate_response_schema response
raise CredentialsError, "The executable response is missing the version field." if response[:version].nil?
if response[:version] > EXECUTABLE_SUPPORTED_MAX_VERSION
raise CredentialsError, "Executable returned unsupported version #{response[:version]}."
end
raise CredentialsError, "The executable response is missing the success field." if response[:success].nil?
end
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
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 CredentialsError,
"Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}"
end
output
end
end
end
end
end
end
|