googleauth 0.17.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -62
  3. data/README.md +49 -14
  4. data/lib/googleauth/application_default.rb +11 -26
  5. data/lib/googleauth/base_client.rb +80 -0
  6. data/lib/googleauth/client_id.rb +10 -25
  7. data/lib/googleauth/compute_engine.rb +10 -25
  8. data/lib/googleauth/credentials.rb +12 -27
  9. data/lib/googleauth/credentials_loader.rb +27 -43
  10. data/lib/googleauth/default_credentials.rb +15 -25
  11. data/lib/googleauth/external_account/aws_credentials.rb +378 -0
  12. data/lib/googleauth/external_account/base_credentials.rb +158 -0
  13. data/lib/googleauth/external_account/external_account_utils.rb +103 -0
  14. data/lib/googleauth/external_account/identity_pool_credentials.rb +118 -0
  15. data/lib/googleauth/external_account/pluggable_credentials.rb +156 -0
  16. data/lib/googleauth/external_account.rb +93 -0
  17. data/lib/googleauth/helpers/connection.rb +35 -0
  18. data/lib/googleauth/iam.rb +10 -25
  19. data/lib/googleauth/id_tokens/errors.rb +9 -23
  20. data/lib/googleauth/id_tokens/key_sources.rb +19 -33
  21. data/lib/googleauth/id_tokens/verifier.rb +9 -23
  22. data/lib/googleauth/id_tokens.rb +11 -25
  23. data/lib/googleauth/json_key_reader.rb +10 -25
  24. data/lib/googleauth/oauth2/sts_client.rb +109 -0
  25. data/lib/googleauth/scope_util.rb +10 -25
  26. data/lib/googleauth/service_account.rb +11 -28
  27. data/lib/googleauth/signet.rb +16 -58
  28. data/lib/googleauth/stores/file_token_store.rb +10 -25
  29. data/lib/googleauth/stores/redis_token_store.rb +10 -25
  30. data/lib/googleauth/token_store.rb +10 -25
  31. data/lib/googleauth/user_authorizer.rb +10 -25
  32. data/lib/googleauth/user_refresh.rb +15 -27
  33. data/lib/googleauth/version.rb +11 -26
  34. data/lib/googleauth/web_user_authorizer.rb +10 -25
  35. data/lib/googleauth.rb +10 -25
  36. 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