googleauth 0.17.1 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +112 -62
- data/README.md +49 -14
- data/lib/googleauth/application_default.rb +11 -26
- data/lib/googleauth/base_client.rb +80 -0
- data/lib/googleauth/client_id.rb +10 -25
- data/lib/googleauth/compute_engine.rb +10 -25
- data/lib/googleauth/credentials.rb +12 -27
- data/lib/googleauth/credentials_loader.rb +27 -43
- data/lib/googleauth/default_credentials.rb +15 -25
- data/lib/googleauth/external_account/aws_credentials.rb +378 -0
- data/lib/googleauth/external_account/base_credentials.rb +158 -0
- data/lib/googleauth/external_account/external_account_utils.rb +103 -0
- data/lib/googleauth/external_account/identity_pool_credentials.rb +118 -0
- data/lib/googleauth/external_account/pluggable_credentials.rb +156 -0
- data/lib/googleauth/external_account.rb +93 -0
- data/lib/googleauth/helpers/connection.rb +35 -0
- data/lib/googleauth/iam.rb +10 -25
- data/lib/googleauth/id_tokens/errors.rb +9 -23
- data/lib/googleauth/id_tokens/key_sources.rb +19 -33
- data/lib/googleauth/id_tokens/verifier.rb +9 -23
- data/lib/googleauth/id_tokens.rb +11 -25
- data/lib/googleauth/json_key_reader.rb +10 -25
- data/lib/googleauth/oauth2/sts_client.rb +109 -0
- data/lib/googleauth/scope_util.rb +10 -25
- data/lib/googleauth/service_account.rb +11 -28
- data/lib/googleauth/signet.rb +16 -58
- data/lib/googleauth/stores/file_token_store.rb +10 -25
- data/lib/googleauth/stores/redis_token_store.rb +10 -25
- data/lib/googleauth/token_store.rb +10 -25
- data/lib/googleauth/user_authorizer.rb +10 -25
- data/lib/googleauth/user_refresh.rb +15 -27
- data/lib/googleauth/version.rb +11 -26
- data/lib/googleauth/web_user_authorizer.rb +10 -25
- data/lib/googleauth.rb +10 -25
- metadata +26 -11
@@ -0,0 +1,158 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.require "time"
|
14
|
+
|
15
|
+
require "googleauth/base_client"
|
16
|
+
require "googleauth/helpers/connection"
|
17
|
+
require "googleauth/oauth2/sts_client"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
# Module Auth provides classes that provide Google-specific authorization
|
21
|
+
# used to access Google APIs.
|
22
|
+
module Auth
|
23
|
+
module ExternalAccount
|
24
|
+
# Authenticates requests using External Account credentials, such
|
25
|
+
# as those provided by the AWS provider or OIDC provider like Azure, etc.
|
26
|
+
module BaseCredentials
|
27
|
+
# Contains all methods needed for all external account credentials.
|
28
|
+
# Other credentials should call `base_setup` during initialization
|
29
|
+
# And should define the :retrieve_subject_token! method
|
30
|
+
|
31
|
+
# External account JSON type identifier.
|
32
|
+
EXTERNAL_ACCOUNT_JSON_TYPE = "external_account".freeze
|
33
|
+
# The token exchange grant_type used for exchanging credentials.
|
34
|
+
STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze
|
35
|
+
# The token exchange requested_token_type. This is always an access_token.
|
36
|
+
STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token".freeze
|
37
|
+
# Default IAM_SCOPE
|
38
|
+
IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
|
39
|
+
|
40
|
+
include Google::Auth::BaseClient
|
41
|
+
include Helpers::Connection
|
42
|
+
|
43
|
+
attr_reader :expires_at
|
44
|
+
attr_accessor :access_token
|
45
|
+
|
46
|
+
def expires_within? seconds
|
47
|
+
# This method is needed for BaseClient
|
48
|
+
@expires_at && @expires_at - Time.now.utc < seconds
|
49
|
+
end
|
50
|
+
|
51
|
+
def expires_at= new_expires_at
|
52
|
+
@expires_at = normalize_timestamp new_expires_at
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch_access_token! _options = {}
|
56
|
+
# This method is needed for BaseClient
|
57
|
+
response = exchange_token
|
58
|
+
|
59
|
+
if @service_account_impersonation_url
|
60
|
+
impersonated_response = get_impersonated_access_token response["access_token"]
|
61
|
+
self.expires_at = impersonated_response["expireTime"]
|
62
|
+
self.access_token = impersonated_response["accessToken"]
|
63
|
+
else
|
64
|
+
# Extract the expiration time in seconds from the response and calculate the actual expiration time
|
65
|
+
# and then save that to the expiry variable.
|
66
|
+
self.expires_at = Time.now.utc + response["expires_in"].to_i
|
67
|
+
self.access_token = response["access_token"]
|
68
|
+
end
|
69
|
+
|
70
|
+
notify_refresh_listeners
|
71
|
+
end
|
72
|
+
|
73
|
+
# Retrieves the subject token using the credential_source object.
|
74
|
+
# @return [string]
|
75
|
+
# The retrieved subject token.
|
76
|
+
#
|
77
|
+
def retrieve_subject_token!
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns whether the credentials represent a workforce pool (True) or
|
82
|
+
# workload (False) based on the credentials' audience.
|
83
|
+
#
|
84
|
+
# @return [bool]
|
85
|
+
# true if the credentials represent a workforce pool.
|
86
|
+
# false if they represent a workload.
|
87
|
+
def is_workforce_pool?
|
88
|
+
pattern = "//iam\.googleapis\.com/locations/[^/]+/workforcePools/"
|
89
|
+
/#{pattern}/.match?(@audience || "")
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def token_type
|
95
|
+
# This method is needed for BaseClient
|
96
|
+
:access_token
|
97
|
+
end
|
98
|
+
|
99
|
+
def base_setup options
|
100
|
+
self.default_connection = options[:connection]
|
101
|
+
|
102
|
+
@audience = options[:audience]
|
103
|
+
@scope = options[:scope] || IAM_SCOPE
|
104
|
+
@subject_token_type = options[:subject_token_type]
|
105
|
+
@token_url = options[:token_url]
|
106
|
+
@token_info_url = options[:token_info_url]
|
107
|
+
@service_account_impersonation_url = options[:service_account_impersonation_url]
|
108
|
+
@service_account_impersonation_options = options[:service_account_impersonation_options] || {}
|
109
|
+
@client_id = options[:client_id]
|
110
|
+
@client_secret = options[:client_secret]
|
111
|
+
@quota_project_id = options[:quota_project_id]
|
112
|
+
@project_id = nil
|
113
|
+
@workforce_pool_user_project = options[:workforce_pool_user_project]
|
114
|
+
|
115
|
+
@expires_at = nil
|
116
|
+
@access_token = nil
|
117
|
+
|
118
|
+
@sts_client = Google::Auth::OAuth2::STSClient.new(
|
119
|
+
token_exchange_endpoint: @token_url,
|
120
|
+
connection: default_connection
|
121
|
+
)
|
122
|
+
return unless @workforce_pool_user_project && !is_workforce_pool?
|
123
|
+
raise "workforce_pool_user_project should not be set for non-workforce pool credentials."
|
124
|
+
end
|
125
|
+
|
126
|
+
def exchange_token
|
127
|
+
additional_options = nil
|
128
|
+
if @client_id.nil? && @workforce_pool_user_project
|
129
|
+
additional_options = { userProject: @workforce_pool_user_project }
|
130
|
+
end
|
131
|
+
@sts_client.exchange_token(
|
132
|
+
audience: @audience,
|
133
|
+
grant_type: STS_GRANT_TYPE,
|
134
|
+
subject_token: retrieve_subject_token!,
|
135
|
+
subject_token_type: @subject_token_type,
|
136
|
+
scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope,
|
137
|
+
requested_token_type: STS_REQUESTED_TOKEN_TYPE,
|
138
|
+
additional_options: additional_options
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
def get_impersonated_access_token token, _options = {}
|
143
|
+
response = connection.post @service_account_impersonation_url do |req|
|
144
|
+
req.headers["Authorization"] = "Bearer #{token}"
|
145
|
+
req.headers["Content-Type"] = "application/json"
|
146
|
+
req.body = MultiJson.dump({ scope: @scope })
|
147
|
+
end
|
148
|
+
|
149
|
+
if response.status != 200
|
150
|
+
raise "Service account impersonation failed with status #{response.status}"
|
151
|
+
end
|
152
|
+
|
153
|
+
MultiJson.load response.body
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.require "time"
|
14
|
+
|
15
|
+
require "googleauth/base_client"
|
16
|
+
require "googleauth/helpers/connection"
|
17
|
+
require "googleauth/oauth2/sts_client"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
# Module Auth provides classes that provide Google-specific authorization
|
21
|
+
# used to access Google APIs.
|
22
|
+
module Auth
|
23
|
+
module ExternalAccount
|
24
|
+
# Authenticates requests using External Account credentials, such
|
25
|
+
# as those provided by the AWS provider or OIDC provider like Azure, etc.
|
26
|
+
module ExternalAccountUtils
|
27
|
+
# Cloud resource manager URL used to retrieve project information.
|
28
|
+
CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/".freeze
|
29
|
+
|
30
|
+
##
|
31
|
+
# Retrieves the project ID corresponding to the workload identity or workforce pool.
|
32
|
+
# For workforce pool credentials, it returns the project ID corresponding to the workforce_pool_user_project.
|
33
|
+
# When not determinable, None is returned.
|
34
|
+
#
|
35
|
+
# The resource may not have permission (resourcemanager.projects.get) to
|
36
|
+
# call this API or the required scopes may not be selected:
|
37
|
+
# https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
|
38
|
+
#
|
39
|
+
# @return [string,nil]
|
40
|
+
# The project ID corresponding to the workload identity pool or workforce pool if determinable.
|
41
|
+
#
|
42
|
+
def project_id
|
43
|
+
return @project_id unless @project_id.nil?
|
44
|
+
project_number = self.project_number || @workforce_pool_user_project
|
45
|
+
|
46
|
+
# if we missing either project number or scope, we won't retrieve project_id
|
47
|
+
return nil if project_number.nil? || @scope.nil?
|
48
|
+
|
49
|
+
url = "#{CLOUD_RESOURCE_MANAGER}#{project_number}"
|
50
|
+
response = connection.get url do |req|
|
51
|
+
req.headers["Authorization"] = "Bearer #{@access_token}"
|
52
|
+
req.headers["Content-Type"] = "application/json"
|
53
|
+
end
|
54
|
+
|
55
|
+
if response.status == 200
|
56
|
+
response_data = MultiJson.load response.body, symbolize_names: true
|
57
|
+
@project_id = response_data[:projectId]
|
58
|
+
end
|
59
|
+
|
60
|
+
@project_id
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Retrieve the project number corresponding to workload identity pool
|
65
|
+
# STS audience pattern:
|
66
|
+
# `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
|
67
|
+
#
|
68
|
+
# @return [string, nil]
|
69
|
+
#
|
70
|
+
def project_number
|
71
|
+
segments = @audience.split "/"
|
72
|
+
idx = segments.index "projects"
|
73
|
+
return nil if idx.nil? || idx + 1 == segments.size
|
74
|
+
segments[idx + 1]
|
75
|
+
end
|
76
|
+
|
77
|
+
def normalize_timestamp time
|
78
|
+
case time
|
79
|
+
when NilClass
|
80
|
+
nil
|
81
|
+
when Time
|
82
|
+
time
|
83
|
+
when String
|
84
|
+
Time.parse time
|
85
|
+
else
|
86
|
+
raise "Invalid time value #{time}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def service_account_email
|
91
|
+
return nil if @service_account_impersonation_url.nil?
|
92
|
+
start_idx = @service_account_impersonation_url.rindex "/"
|
93
|
+
end_idx = @service_account_impersonation_url.index ":generateAccessToken"
|
94
|
+
if start_idx != -1 && end_idx != -1 && start_idx < end_idx
|
95
|
+
start_idx += 1
|
96
|
+
return @service_account_impersonation_url[start_idx..end_idx]
|
97
|
+
end
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "time"
|
16
|
+
require "googleauth/external_account/base_credentials"
|
17
|
+
require "googleauth/external_account/external_account_utils"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
# Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
|
21
|
+
module Auth
|
22
|
+
module ExternalAccount
|
23
|
+
# This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI
|
24
|
+
# provider then exchanging the credentials for a short-lived Google Cloud access token.
|
25
|
+
class IdentityPoolCredentials
|
26
|
+
include Google::Auth::ExternalAccount::BaseCredentials
|
27
|
+
include Google::Auth::ExternalAccount::ExternalAccountUtils
|
28
|
+
extend CredentialsLoader
|
29
|
+
|
30
|
+
# Will always be nil, but method still gets used.
|
31
|
+
attr_reader :client_id
|
32
|
+
|
33
|
+
# Initialize from options map.
|
34
|
+
#
|
35
|
+
# @param [string] audience
|
36
|
+
# @param [hash{symbol => value}] credential_source
|
37
|
+
# credential_source is a hash that contains either source file or url.
|
38
|
+
# credential_source_format is either text or json. To define how we parse the credential response.
|
39
|
+
#
|
40
|
+
def initialize options = {}
|
41
|
+
base_setup options
|
42
|
+
|
43
|
+
@audience = options[:audience]
|
44
|
+
@credential_source = options[:credential_source] || {}
|
45
|
+
@credential_source_file = @credential_source[:file]
|
46
|
+
@credential_source_url = @credential_source[:url]
|
47
|
+
@credential_source_headers = @credential_source[:headers] || {}
|
48
|
+
@credential_source_format = @credential_source[:format] || {}
|
49
|
+
@credential_source_format_type = @credential_source_format[:type] || "text"
|
50
|
+
validate_credential_source
|
51
|
+
end
|
52
|
+
|
53
|
+
# Implementation of BaseCredentials retrieve_subject_token!
|
54
|
+
def retrieve_subject_token!
|
55
|
+
content, resource_name = token_data
|
56
|
+
if @credential_source_format_type == "text"
|
57
|
+
token = content
|
58
|
+
else
|
59
|
+
begin
|
60
|
+
response_data = MultiJson.load content, symbolize_keys: true
|
61
|
+
token = response_data[@credential_source_field_name.to_sym]
|
62
|
+
rescue StandardError
|
63
|
+
raise "Unable to parse subject_token from JSON resource #{resource_name} " \
|
64
|
+
"using key #{@credential_source_field_name}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
raise "Missing subject_token in the credential_source file/response." unless token
|
68
|
+
token
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def validate_credential_source
|
74
|
+
# `environment_id` is only supported in AWS or dedicated future external account credentials.
|
75
|
+
unless @credential_source[:environment_id].nil?
|
76
|
+
raise "Invalid Identity Pool credential_source field 'environment_id'"
|
77
|
+
end
|
78
|
+
unless ["json", "text"].include? @credential_source_format_type
|
79
|
+
raise "Invalid credential_source format #{@credential_source_format_type}"
|
80
|
+
end
|
81
|
+
# for JSON types, get the required subject_token field name.
|
82
|
+
@credential_source_field_name = @credential_source_format[:subject_token_field_name]
|
83
|
+
if @credential_source_format_type == "json" && @credential_source_field_name.nil?
|
84
|
+
raise "Missing subject_token_field_name for JSON credential_source format"
|
85
|
+
end
|
86
|
+
# check file or url must be fulfilled and mutually exclusiveness.
|
87
|
+
if @credential_source_file && @credential_source_url
|
88
|
+
raise "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
|
89
|
+
end
|
90
|
+
return unless (@credential_source_file || @credential_source_url).nil?
|
91
|
+
raise "Missing credential_source. A 'file' or 'url' must be provided."
|
92
|
+
end
|
93
|
+
|
94
|
+
def token_data
|
95
|
+
@credential_source_file.nil? ? url_data : file_data
|
96
|
+
end
|
97
|
+
|
98
|
+
def file_data
|
99
|
+
raise "File #{@credential_source_file} was not found." unless File.exist? @credential_source_file
|
100
|
+
content = File.read @credential_source_file, encoding: "utf-8"
|
101
|
+
[content, @credential_source_file]
|
102
|
+
end
|
103
|
+
|
104
|
+
def url_data
|
105
|
+
begin
|
106
|
+
response = connection.get @credential_source_url do |req|
|
107
|
+
req.headers.merge! @credential_source_headers
|
108
|
+
end
|
109
|
+
rescue Faraday::Error => e
|
110
|
+
raise "Error retrieving from credential url: #{e}"
|
111
|
+
end
|
112
|
+
raise "Unable to retrieve Identity Pool subject token #{response.body}" unless response.success?
|
113
|
+
[response.body, @credential_source_url]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "open3"
|
16
|
+
require "time"
|
17
|
+
require "googleauth/external_account/base_credentials"
|
18
|
+
require "googleauth/external_account/external_account_utils"
|
19
|
+
|
20
|
+
module Google
|
21
|
+
# Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
|
22
|
+
module Auth
|
23
|
+
module ExternalAccount
|
24
|
+
# This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI
|
25
|
+
# provider then exchanging the credentials for a short-lived Google Cloud access token.
|
26
|
+
class PluggableAuthCredentials
|
27
|
+
# constant for pluggable auth enablement in environment variable.
|
28
|
+
ENABLE_PLUGGABLE_ENV = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES".freeze
|
29
|
+
EXECUTABLE_SUPPORTED_MAX_VERSION = 1
|
30
|
+
EXECUTABLE_TIMEOUT_MILLIS_DEFAULT = 30 * 1000
|
31
|
+
EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 1000
|
32
|
+
EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND = 120 * 1000
|
33
|
+
ID_TOKEN_TYPE = ["urn:ietf:params:oauth:token-type:jwt", "urn:ietf:params:oauth:token-type:id_token"].freeze
|
34
|
+
|
35
|
+
include Google::Auth::ExternalAccount::BaseCredentials
|
36
|
+
include Google::Auth::ExternalAccount::ExternalAccountUtils
|
37
|
+
extend CredentialsLoader
|
38
|
+
|
39
|
+
# Will always be nil, but method still gets used.
|
40
|
+
attr_reader :client_id
|
41
|
+
|
42
|
+
# Initialize from options map.
|
43
|
+
#
|
44
|
+
# @param [string] audience
|
45
|
+
# @param [hash{symbol => value}] credential_source
|
46
|
+
# credential_source is a hash that contains either source file or url.
|
47
|
+
# credential_source_format is either text or json. To define how we parse the credential response.
|
48
|
+
#
|
49
|
+
def initialize options = {}
|
50
|
+
base_setup options
|
51
|
+
|
52
|
+
@audience = options[:audience]
|
53
|
+
@credential_source = options[:credential_source] || {}
|
54
|
+
@credential_source_executable = @credential_source[:executable]
|
55
|
+
raise "Missing excutable source. An 'executable' must be provided" if @credential_source_executable.nil?
|
56
|
+
@credential_source_executable_command = @credential_source_executable[:command]
|
57
|
+
if @credential_source_executable_command.nil?
|
58
|
+
raise "Missing command field. Executable command must be provided."
|
59
|
+
end
|
60
|
+
@credential_source_executable_timeout_millis = @credential_source_executable[:timeout_millis] ||
|
61
|
+
EXECUTABLE_TIMEOUT_MILLIS_DEFAULT
|
62
|
+
if @credential_source_executable_timeout_millis < EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND ||
|
63
|
+
@credential_source_executable_timeout_millis > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND
|
64
|
+
raise "Timeout must be between 5 and 120 seconds."
|
65
|
+
end
|
66
|
+
@credential_source_executable_output_file = @credential_source_executable[:output_file]
|
67
|
+
end
|
68
|
+
|
69
|
+
def retrieve_subject_token!
|
70
|
+
unless ENV[ENABLE_PLUGGABLE_ENV] == "1"
|
71
|
+
raise "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') " \
|
72
|
+
"to run."
|
73
|
+
end
|
74
|
+
# check output file first
|
75
|
+
subject_token = load_subject_token_from_output_file
|
76
|
+
return subject_token unless subject_token.nil?
|
77
|
+
# environment variable injection
|
78
|
+
env = inject_environment_variables
|
79
|
+
output = subprocess_with_timeout env, @credential_source_executable_command,
|
80
|
+
@credential_source_executable_timeout_millis
|
81
|
+
response = MultiJson.load output, symbolize_keys: true
|
82
|
+
parse_subject_token response
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def load_subject_token_from_output_file
|
88
|
+
return nil if @credential_source_executable_output_file.nil?
|
89
|
+
return nil unless File.exist? @credential_source_executable_output_file
|
90
|
+
begin
|
91
|
+
content = File.read @credential_source_executable_output_file, encoding: "utf-8"
|
92
|
+
response = MultiJson.load content, symbolize_keys: true
|
93
|
+
rescue StandardError
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
begin
|
97
|
+
subject_token = parse_subject_token response
|
98
|
+
rescue StandardError => e
|
99
|
+
return nil if e.message.match(/The token returned by the executable is expired/)
|
100
|
+
raise e
|
101
|
+
end
|
102
|
+
subject_token
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_subject_token response
|
106
|
+
validate_response_schema response
|
107
|
+
unless response[:success]
|
108
|
+
if response[:code].nil? || response[:message].nil?
|
109
|
+
raise "Error code and message fields are required in the response."
|
110
|
+
end
|
111
|
+
raise "Executable returned unsuccessful response: code: #{response[:code]}, message: #{response[:message]}."
|
112
|
+
end
|
113
|
+
if response[:expiration_time] && response[:expiration_time] < Time.now.to_i
|
114
|
+
raise "The token returned by the executable is expired."
|
115
|
+
end
|
116
|
+
raise "The executable response is missing the token_type field." if response[:token_type].nil?
|
117
|
+
return response[:id_token] if ID_TOKEN_TYPE.include? response[:token_type]
|
118
|
+
return response[:saml_response] if response[:token_type] == "urn:ietf:params:oauth:token-type:saml2"
|
119
|
+
raise "Executable returned unsupported token type."
|
120
|
+
end
|
121
|
+
|
122
|
+
def validate_response_schema response
|
123
|
+
raise "The executable response is missing the version field." if response[:version].nil?
|
124
|
+
if response[:version] > EXECUTABLE_SUPPORTED_MAX_VERSION
|
125
|
+
raise "Executable returned unsupported version #{response[:version]}."
|
126
|
+
end
|
127
|
+
raise "The executable response is missing the success field." if response[:success].nil?
|
128
|
+
end
|
129
|
+
|
130
|
+
def inject_environment_variables
|
131
|
+
env = ENV.to_h
|
132
|
+
env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = @audience
|
133
|
+
env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = @subject_token_type
|
134
|
+
env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0" # only non-interactive mode we support.
|
135
|
+
unless @service_account_impersonation_url.nil?
|
136
|
+
env["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = service_account_email
|
137
|
+
end
|
138
|
+
unless @credential_source_executable_output_file.nil?
|
139
|
+
env["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = @credential_source_executable_output_file
|
140
|
+
end
|
141
|
+
env
|
142
|
+
end
|
143
|
+
|
144
|
+
def subprocess_with_timeout environment_vars, command, timeout_seconds
|
145
|
+
Timeout.timeout timeout_seconds do
|
146
|
+
output, error, status = Open3.capture3 environment_vars, command
|
147
|
+
unless status.success?
|
148
|
+
raise "Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}"
|
149
|
+
end
|
150
|
+
output
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# Copyright 2022 Google, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "time"
|
16
|
+
require "uri"
|
17
|
+
require "googleauth/credentials_loader"
|
18
|
+
require "googleauth/external_account/aws_credentials"
|
19
|
+
require "googleauth/external_account/identity_pool_credentials"
|
20
|
+
require "googleauth/external_account/pluggable_credentials"
|
21
|
+
|
22
|
+
module Google
|
23
|
+
# Module Auth provides classes that provide Google-specific authorization
|
24
|
+
# used to access Google APIs.
|
25
|
+
module Auth
|
26
|
+
# Authenticates requests using External Account credentials, such
|
27
|
+
# as those provided by the AWS provider.
|
28
|
+
module ExternalAccount
|
29
|
+
# Provides an entrypoint for all Exernal Account credential classes.
|
30
|
+
class Credentials
|
31
|
+
# The subject token type used for AWS external_account credentials.
|
32
|
+
AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request".freeze
|
33
|
+
MISSING_CREDENTIAL_SOURCE = "missing credential source for external account".freeze
|
34
|
+
INVALID_EXTERNAL_ACCOUNT_TYPE = "credential source is not supported external account type".freeze
|
35
|
+
|
36
|
+
# Create a ExternalAccount::Credentials
|
37
|
+
#
|
38
|
+
# @param json_key_io [IO] an IO from which the JSON key can be read
|
39
|
+
# @param scope [String,Array,nil] the scope(s) to access
|
40
|
+
def self.make_creds options = {}
|
41
|
+
json_key_io, scope = options.values_at :json_key_io, :scope
|
42
|
+
|
43
|
+
raise "A json file is required for external account credentials." unless json_key_io
|
44
|
+
user_creds = read_json_key json_key_io
|
45
|
+
|
46
|
+
# AWS credentials is determined by aws subject token type
|
47
|
+
return make_aws_credentials user_creds, scope if user_creds[:subject_token_type] == AWS_SUBJECT_TOKEN_TYPE
|
48
|
+
|
49
|
+
raise MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
|
50
|
+
user_creds[:scope] = scope
|
51
|
+
make_external_account_credentials user_creds
|
52
|
+
end
|
53
|
+
|
54
|
+
# Reads the required fields from the JSON.
|
55
|
+
def self.read_json_key json_key_io
|
56
|
+
json_key = MultiJson.load json_key_io.read, symbolize_keys: true
|
57
|
+
wanted = [
|
58
|
+
:audience, :subject_token_type, :token_url, :credential_source
|
59
|
+
]
|
60
|
+
wanted.each do |key|
|
61
|
+
raise "the json is missing the #{key} field" unless json_key.key? key
|
62
|
+
end
|
63
|
+
json_key
|
64
|
+
end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
private
|
68
|
+
|
69
|
+
def make_aws_credentials user_creds, scope
|
70
|
+
Google::Auth::ExternalAccount::AwsCredentials.new(
|
71
|
+
audience: user_creds[:audience],
|
72
|
+
scope: scope,
|
73
|
+
subject_token_type: user_creds[:subject_token_type],
|
74
|
+
token_url: user_creds[:token_url],
|
75
|
+
credential_source: user_creds[:credential_source],
|
76
|
+
service_account_impersonation_url: user_creds[:service_account_impersonation_url]
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def make_external_account_credentials user_creds
|
81
|
+
unless user_creds[:credential_source][:file].nil? && user_creds[:credential_source][:url].nil?
|
82
|
+
return Google::Auth::ExternalAccount::IdentityPoolCredentials.new user_creds
|
83
|
+
end
|
84
|
+
unless user_creds[:credential_source][:executable].nil?
|
85
|
+
return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds
|
86
|
+
end
|
87
|
+
raise INVALID_EXTERNAL_ACCOUNT_TYPE
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Copyright 2023 Google, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "faraday"
|
16
|
+
|
17
|
+
module Google
|
18
|
+
# Module Auth provides classes that provide Google-specific authorization
|
19
|
+
# used to access Google APIs.
|
20
|
+
module Auth
|
21
|
+
# Helpers provides utility methods for Google::Auth.
|
22
|
+
module Helpers
|
23
|
+
# Connection provides a Faraday connection for use with Google::Auth.
|
24
|
+
module Connection
|
25
|
+
module_function
|
26
|
+
|
27
|
+
attr_accessor :default_connection
|
28
|
+
|
29
|
+
def connection
|
30
|
+
@default_connection || Faraday.default_connection
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|