googleauth 0.17.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
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