-
Notifications
You must be signed in to change notification settings - Fork 252
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Adding support for pluggable auth credentials (#437)
- Loading branch information
1 parent
28a2f76
commit fb23a65
Showing
5 changed files
with
724 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
lib/googleauth/external_account/pluggable_credentials.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
# 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/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 [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. | ||
# | ||
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 | ||
|
||
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 | ||
|
||
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 | ||
|
||
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 | ||
|
||
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 | ||
|
||
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 "Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}" | ||
end | ||
output | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.