googleauth 1.14.0 → 1.15.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/Credentials.md +110 -0
- data/Errors.md +152 -0
- data/lib/googleauth/api_key.rb +9 -0
- data/lib/googleauth/application_default.rb +3 -1
- data/lib/googleauth/base_client.rb +5 -0
- data/lib/googleauth/bearer_token.rb +16 -2
- data/lib/googleauth/client_id.rb +9 -5
- data/lib/googleauth/compute_engine.rb +64 -18
- data/lib/googleauth/credentials.rb +67 -35
- data/lib/googleauth/credentials_loader.rb +24 -4
- data/lib/googleauth/default_credentials.rb +64 -32
- data/lib/googleauth/errors.rb +117 -0
- data/lib/googleauth/external_account/aws_credentials.rb +85 -18
- data/lib/googleauth/external_account/base_credentials.rb +31 -2
- data/lib/googleauth/external_account/external_account_utils.rb +15 -4
- data/lib/googleauth/external_account/identity_pool_credentials.rb +40 -15
- data/lib/googleauth/external_account/pluggable_credentials.rb +34 -19
- data/lib/googleauth/external_account.rb +35 -6
- data/lib/googleauth/iam.rb +19 -3
- data/lib/googleauth/id_tokens/errors.rb +13 -7
- data/lib/googleauth/id_tokens/key_sources.rb +13 -7
- data/lib/googleauth/id_tokens/verifier.rb +2 -3
- data/lib/googleauth/id_tokens.rb +4 -4
- data/lib/googleauth/impersonated_service_account.rb +64 -17
- data/lib/googleauth/json_key_reader.rb +11 -2
- data/lib/googleauth/oauth2/sts_client.rb +9 -4
- data/lib/googleauth/scope_util.rb +1 -1
- data/lib/googleauth/service_account.rb +37 -10
- data/lib/googleauth/service_account_jwt_header.rb +9 -2
- data/lib/googleauth/signet.rb +24 -6
- data/lib/googleauth/user_authorizer.rb +35 -7
- data/lib/googleauth/user_refresh.rb +42 -16
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +46 -9
- data/lib/googleauth.rb +1 -0
- metadata +8 -5
@@ -14,9 +14,12 @@
|
|
14
14
|
|
15
15
|
require "forwardable"
|
16
16
|
require "json"
|
17
|
+
require "pathname"
|
17
18
|
require "signet/oauth_2/client"
|
19
|
+
require "multi_json"
|
18
20
|
|
19
21
|
require "googleauth/credentials_loader"
|
22
|
+
require "googleauth/errors"
|
20
23
|
|
21
24
|
module Google
|
22
25
|
module Auth
|
@@ -357,12 +360,13 @@ module Google
|
|
357
360
|
# Creates a new Credentials instance with the provided auth credentials, and with the default
|
358
361
|
# values configured on the class.
|
359
362
|
#
|
360
|
-
# @param [String, Hash,
|
363
|
+
# @param [String, Pathname, Hash, Google::Auth::BaseClient] source_creds
|
361
364
|
# The source of credentials. It can be provided as one of the following:
|
362
365
|
#
|
363
|
-
# * The path to a JSON keyfile (as a `String`)
|
366
|
+
# * The path to a JSON keyfile (as a `String` or a `Pathname`)
|
364
367
|
# * The contents of a JSON keyfile (as a `Hash`)
|
365
|
-
# * A `
|
368
|
+
# * A `Google::Auth::BaseClient` credentials object, including but not limited to
|
369
|
+
# a `Signet::OAuth2::Client` object.
|
366
370
|
# * Any credentials object that supports the methods this wrapper delegates to an inner client.
|
367
371
|
#
|
368
372
|
# If this parameter is an object (`Signet::OAuth2::Client` or other) it will be used as an inner client.
|
@@ -391,14 +395,20 @@ module Google
|
|
391
395
|
# parameters of the `Signet::OAuth2::Client`, such as connection parameters,
|
392
396
|
# timeouts, etc.
|
393
397
|
#
|
398
|
+
# @raise [Google::Auth::InitializationError] If source_creds is nil
|
399
|
+
# @raise [ArgumentError] If both scope and target_audience are specified
|
400
|
+
#
|
394
401
|
def initialize source_creds, options = {}
|
395
|
-
|
402
|
+
if source_creds.nil?
|
403
|
+
raise InitializationError,
|
404
|
+
"The source credentials passed to Google::Auth::Credentials.new were nil."
|
405
|
+
end
|
396
406
|
|
397
407
|
options = symbolize_hash_keys options
|
398
408
|
@project_id = options[:project_id] || options[:project]
|
399
409
|
@quota_project_id = options[:quota_project_id]
|
400
410
|
case source_creds
|
401
|
-
when String
|
411
|
+
when String, Pathname
|
402
412
|
update_from_filepath source_creds, options
|
403
413
|
when Hash
|
404
414
|
update_from_hash source_creds, options
|
@@ -499,17 +509,61 @@ module Google
|
|
499
509
|
token_credential_uri: options[:token_credential_uri] || token_credential_uri,
|
500
510
|
audience: options[:audience] || audience
|
501
511
|
}
|
502
|
-
|
512
|
+
|
513
|
+
# Determine the class, which consumes the IO stream
|
514
|
+
json_key, clz = Google::Auth::DefaultCredentials.determine_creds_class creds_input[:json_key_io]
|
515
|
+
|
516
|
+
# Re-serialize the parsed JSON and replace the IO stream in creds_input
|
517
|
+
creds_input[:json_key_io] = StringIO.new MultiJson.dump(json_key)
|
518
|
+
|
519
|
+
client = clz.make_creds creds_input
|
503
520
|
options = options.select { |k, _v| k == :logger }
|
504
521
|
new client, options
|
505
522
|
end
|
506
523
|
|
524
|
+
# @private
|
525
|
+
# Initializes the Signet client.
|
526
|
+
def self.init_client hash, options = {}
|
527
|
+
options = update_client_options options
|
528
|
+
io = StringIO.new JSON.generate hash
|
529
|
+
|
530
|
+
# Determine the class, which consumes the IO stream
|
531
|
+
json_key, clz = Google::Auth::DefaultCredentials.determine_creds_class io
|
532
|
+
|
533
|
+
# Re-serialize the parsed JSON and create a new IO stream.
|
534
|
+
new_io = StringIO.new MultiJson.dump(json_key)
|
535
|
+
|
536
|
+
clz.make_creds options.merge!(json_key_io: new_io)
|
537
|
+
end
|
538
|
+
|
539
|
+
# @private
|
540
|
+
# Updates client options with defaults from the credential class
|
541
|
+
#
|
542
|
+
# @param [Hash] options Options to update
|
543
|
+
# @return [Hash] Updated options hash
|
544
|
+
# @raise [ArgumentError] If both scope and target_audience are specified
|
545
|
+
def self.update_client_options options
|
546
|
+
options = options.dup
|
547
|
+
|
548
|
+
# options have higher priority over constructor defaults
|
549
|
+
options[:token_credential_uri] ||= token_credential_uri
|
550
|
+
options[:audience] ||= audience
|
551
|
+
options[:scope] ||= scope
|
552
|
+
options[:target_audience] ||= target_audience
|
553
|
+
|
554
|
+
if !Array(options[:scope]).empty? && options[:target_audience]
|
555
|
+
raise ArgumentError, "Cannot specify both scope and target_audience"
|
556
|
+
end
|
557
|
+
options.delete :scope unless options[:target_audience].nil?
|
558
|
+
|
559
|
+
options
|
560
|
+
end
|
561
|
+
|
507
562
|
private_class_method :from_env_vars,
|
508
563
|
:from_default_paths,
|
509
564
|
:from_application_default,
|
510
565
|
:from_io
|
511
566
|
|
512
|
-
|
513
567
|
# Creates a duplicate of these credentials. This method tries to create the duplicate of the
|
514
568
|
# wrapped credentials if they support duplication and use them as is if they don't.
|
515
569
|
#
|
@@ -554,17 +608,12 @@ module Google
|
|
554
608
|
protected
|
555
609
|
|
556
610
|
# Verify that the keyfile argument is a file.
|
611
|
+
#
|
612
|
+
# @param [String] keyfile Path to the keyfile
|
613
|
+
# @raise [Google::Auth::InitializationError] If the keyfile does not exist
|
557
614
|
def verify_keyfile_exists! keyfile
|
558
615
|
exists = ::File.file? keyfile
|
559
|
-
raise "The keyfile '#{keyfile}' is not a valid file." unless exists
|
560
|
-
end
|
561
|
-
|
562
|
-
# Initializes the Signet client.
|
563
|
-
def init_client hash, options = {}
|
564
|
-
options = update_client_options options
|
565
|
-
io = StringIO.new JSON.generate hash
|
566
|
-
options.merge! json_key_io: io
|
567
|
-
Google::Auth::DefaultCredentials.make_creds options
|
616
|
+
raise InitializationError, "The keyfile '#{keyfile}' is not a valid file." unless exists
|
568
617
|
end
|
569
618
|
|
570
619
|
# returns a new Hash with string keys instead of symbol keys.
|
@@ -577,23 +626,6 @@ module Google
|
|
577
626
|
hash.to_h.transform_keys(&:to_sym)
|
578
627
|
end
|
579
628
|
|
580
|
-
def update_client_options options
|
581
|
-
options = options.dup
|
582
|
-
|
583
|
-
# options have higher priority over constructor defaults
|
584
|
-
options[:token_credential_uri] ||= self.class.token_credential_uri
|
585
|
-
options[:audience] ||= self.class.audience
|
586
|
-
options[:scope] ||= self.class.scope
|
587
|
-
options[:target_audience] ||= self.class.target_audience
|
588
|
-
|
589
|
-
if !Array(options[:scope]).empty? && options[:target_audience]
|
590
|
-
raise ArgumentError, "Cannot specify both scope and target_audience"
|
591
|
-
end
|
592
|
-
options.delete :scope unless options[:target_audience].nil?
|
593
|
-
|
594
|
-
options
|
595
|
-
end
|
596
|
-
|
597
629
|
def update_from_client client
|
598
630
|
@project_id ||= client.project_id if client.respond_to? :project_id
|
599
631
|
@quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
|
@@ -607,7 +639,7 @@ module Google
|
|
607
639
|
hash["target_audience"] ||= options[:target_audience]
|
608
640
|
@project_id ||= hash["project_id"] || hash["project"]
|
609
641
|
@quota_project_id ||= hash["quota_project_id"]
|
610
|
-
@client = init_client hash, options
|
642
|
+
@client = self.class.init_client hash, options
|
611
643
|
end
|
612
644
|
|
613
645
|
def update_from_filepath path, options
|
@@ -617,7 +649,7 @@ module Google
|
|
617
649
|
json["target_audience"] ||= options[:target_audience]
|
618
650
|
@project_id ||= json["project_id"] || json["project"]
|
619
651
|
@quota_project_id ||= json["quota_project_id"]
|
620
|
-
@client = init_client json, options
|
652
|
+
@client = self.class.init_client json, options
|
621
653
|
end
|
622
654
|
|
623
655
|
def setup_logging logger: :default
|
@@ -15,6 +15,8 @@
|
|
15
15
|
require "os"
|
16
16
|
require "rbconfig"
|
17
17
|
|
18
|
+
require "googleauth/errors"
|
19
|
+
|
18
20
|
module Google
|
19
21
|
# Module Auth provides classes that provide Google-specific authorization
|
20
22
|
# used to access Google APIs.
|
@@ -71,11 +73,12 @@ module Google
|
|
71
73
|
# The following keys are recognized:
|
72
74
|
# * `:default_connection` The connection object to use.
|
73
75
|
# * `:connection_builder` A `Proc` that returns a connection.
|
76
|
+
# @raise [Google::Auth::InitializationError] If the credentials file cannot be read
|
74
77
|
def from_env scope = nil, options = {}
|
75
78
|
options = interpret_options scope, options
|
76
79
|
if ENV.key?(ENV_VAR) && !ENV[ENV_VAR].empty?
|
77
80
|
path = ENV[ENV_VAR]
|
78
|
-
raise "file #{path} does not exist" unless File.exist? path
|
81
|
+
raise InitializationError, "file #{path} does not exist" unless File.exist? path
|
79
82
|
File.open path do |f|
|
80
83
|
return make_creds options.merge(json_key_io: f)
|
81
84
|
end
|
@@ -83,7 +86,7 @@ module Google
|
|
83
86
|
make_creds options
|
84
87
|
end
|
85
88
|
rescue StandardError => e
|
86
|
-
raise "#{NOT_FOUND_ERROR}: #{e}"
|
89
|
+
raise InitializationError, "#{NOT_FOUND_ERROR}: #{e}"
|
87
90
|
end
|
88
91
|
|
89
92
|
# Creates an instance from a well known path.
|
@@ -97,6 +100,7 @@ module Google
|
|
97
100
|
# The following keys are recognized:
|
98
101
|
# * `:default_connection` The connection object to use.
|
99
102
|
# * `:connection_builder` A `Proc` that returns a connection.
|
103
|
+
# @raise [Google::Auth::InitializationError] If the credentials file cannot be read
|
100
104
|
def from_well_known_path scope = nil, options = {}
|
101
105
|
options = interpret_options scope, options
|
102
106
|
home_var = OS.windows? ? "APPDATA" : "HOME"
|
@@ -109,7 +113,7 @@ module Google
|
|
109
113
|
return make_creds options.merge(json_key_io: f)
|
110
114
|
end
|
111
115
|
rescue StandardError => e
|
112
|
-
raise "#{WELL_KNOWN_ERROR}: #{e}"
|
116
|
+
raise InitializationError, "#{WELL_KNOWN_ERROR}: #{e}"
|
113
117
|
end
|
114
118
|
|
115
119
|
# Creates an instance from the system default path
|
@@ -123,6 +127,7 @@ module Google
|
|
123
127
|
# The following keys are recognized:
|
124
128
|
# * `:default_connection` The connection object to use.
|
125
129
|
# * `:connection_builder` A `Proc` that returns a connection.
|
130
|
+
# @raise [Google::Auth::InitializationError] If the credentials file cannot be read or is invalid
|
126
131
|
def from_system_default_path scope = nil, options = {}
|
127
132
|
options = interpret_options scope, options
|
128
133
|
if OS.windows?
|
@@ -137,7 +142,7 @@ module Google
|
|
137
142
|
return make_creds options.merge(json_key_io: f)
|
138
143
|
end
|
139
144
|
rescue StandardError => e
|
140
|
-
raise "#{SYSTEM_DEFAULT_ERROR}: #{e}"
|
145
|
+
raise InitializationError, "#{SYSTEM_DEFAULT_ERROR}: #{e}"
|
141
146
|
end
|
142
147
|
|
143
148
|
module_function
|
@@ -153,6 +158,21 @@ module Google
|
|
153
158
|
nil
|
154
159
|
end
|
155
160
|
|
161
|
+
# @private
|
162
|
+
# Loads a JSON key from an IO object, verifies its type, and rewinds the IO.
|
163
|
+
#
|
164
|
+
# @param json_key_io [IO] An IO object containing the JSON key.
|
165
|
+
# @param expected_type [String] The expected credential type name.
|
166
|
+
# @raise [Google::Auth::InitializationError] If the JSON key type does not match the expected type.
|
167
|
+
def load_and_verify_json_key_type json_key_io, expected_type
|
168
|
+
json_key = MultiJson.load json_key_io.read
|
169
|
+
json_key_io.rewind # Rewind the stream so it can be read again.
|
170
|
+
return if json_key["type"] == expected_type
|
171
|
+
raise Google::Auth::InitializationError,
|
172
|
+
"The provided credentials were not of type '#{expected_type}'. " \
|
173
|
+
"Instead, the type was '#{json_key['type']}'."
|
174
|
+
end
|
175
|
+
|
156
176
|
private
|
157
177
|
|
158
178
|
def interpret_options scope, options
|
@@ -16,6 +16,7 @@ require "multi_json"
|
|
16
16
|
require "stringio"
|
17
17
|
|
18
18
|
require "googleauth/credentials_loader"
|
19
|
+
require "googleauth/errors"
|
19
20
|
require "googleauth/external_account"
|
20
21
|
require "googleauth/service_account"
|
21
22
|
require "googleauth/service_account_jwt_header"
|
@@ -42,50 +43,81 @@ module Google
|
|
42
43
|
# information, refer to [Validate credential configurations from external
|
43
44
|
# sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
44
45
|
#
|
46
|
+
# @deprecated This method is deprecated and will be removed in a future version.
|
47
|
+
# Please use the `make_creds` method on the specific credential class you intend to load,
|
48
|
+
# e.g., `Google::Auth::ServiceAccountCredentials.make_creds`.
|
49
|
+
#
|
50
|
+
# This method does not validate the credential configuration. The security
|
51
|
+
# risk occurs when a credential configuration is accepted from a source that
|
52
|
+
# is not under your control and used without validation on your side.
|
53
|
+
#
|
54
|
+
# If you know that you will be loading credential configurations of a
|
55
|
+
# specific type, it is recommended to use a credential-type-specific
|
56
|
+
# `make_creds` method.
|
57
|
+
# This will ensure that an unexpected credential type with potential for
|
58
|
+
# malicious intent is not loaded unintentionally. You might still have to do
|
59
|
+
# validation for certain credential types. Please follow the recommendation
|
60
|
+
# for that method. For example, if you want to load only service accounts,
|
61
|
+
# you can use:
|
62
|
+
# ```
|
63
|
+
# creds = Google::Auth::ServiceAccountCredentials.make_creds
|
64
|
+
# ```
|
65
|
+
# @see Google::Auth::ServiceAccountCredentials.make_creds
|
66
|
+
#
|
67
|
+
# If you are loading your credential configuration from an untrusted source and have
|
68
|
+
# not mitigated the risks (e.g. by validating the configuration yourself), make
|
69
|
+
# these changes as soon as possible to prevent security risks to your environment.
|
70
|
+
#
|
71
|
+
# Regardless of the method used, it is always your responsibility to validate
|
72
|
+
# configurations received from external sources.
|
73
|
+
#
|
74
|
+
# See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials for more details.
|
75
|
+
#
|
76
|
+
# @param options [Hash] Options for creating the credentials
|
77
|
+
# @return [Google::Auth::Credentials] The credentials instance
|
78
|
+
# @raise [Google::Auth::InitializationError] If the credentials cannot be determined
|
45
79
|
def self.make_creds options = {}
|
46
80
|
json_key_io = options[:json_key_io]
|
47
|
-
|
48
|
-
|
81
|
+
json_key, clz = determine_creds_class json_key_io
|
82
|
+
if json_key
|
49
83
|
io = StringIO.new MultiJson.dump(json_key)
|
50
84
|
clz.make_creds options.merge(json_key_io: io)
|
51
85
|
else
|
52
|
-
clz = read_creds
|
53
86
|
clz.make_creds options
|
54
87
|
end
|
55
88
|
end
|
56
89
|
|
57
|
-
def self.read_creds
|
58
|
-
env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
|
59
|
-
type = ENV[env_var]
|
60
|
-
raise "#{env_var} is undefined in env" unless type
|
61
|
-
case type
|
62
|
-
when "service_account"
|
63
|
-
ServiceAccountCredentials
|
64
|
-
when "authorized_user"
|
65
|
-
UserRefreshCredentials
|
66
|
-
when "external_account"
|
67
|
-
ExternalAccount::Credentials
|
68
|
-
else
|
69
|
-
raise "credentials type '#{type}' is not supported"
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
90
|
# Reads the input json and determines which creds class to use.
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
[
|
91
|
+
#
|
92
|
+
# @param json_key_io [IO, nil] An optional IO object containing the JSON key.
|
93
|
+
# If nil, the credential type is determined from environment variables.
|
94
|
+
# @return [Array(Hash, Class)] The JSON key (or nil if from environment) and the credential class to use
|
95
|
+
# @raise [Google::Auth::InitializationError] If the JSON is missing the type field or has an unsupported type,
|
96
|
+
# or if the environment variable is undefined or unsupported.
|
97
|
+
def self.determine_creds_class json_key_io = nil
|
98
|
+
if json_key_io
|
99
|
+
json_key = MultiJson.load json_key_io.read
|
100
|
+
key = "type"
|
101
|
+
raise InitializationError, "the json is missing the '#{key}' field" unless json_key.key? key
|
102
|
+
type = json_key[key]
|
86
103
|
else
|
87
|
-
|
104
|
+
env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
|
105
|
+
type = ENV[env_var]
|
106
|
+
raise InitializationError, "#{env_var} is undefined in env" unless type
|
107
|
+
json_key = nil
|
88
108
|
end
|
109
|
+
|
110
|
+
clz = case type
|
111
|
+
when ServiceAccountCredentials::CREDENTIAL_TYPE_NAME
|
112
|
+
ServiceAccountCredentials
|
113
|
+
when UserRefreshCredentials::CREDENTIAL_TYPE_NAME
|
114
|
+
UserRefreshCredentials
|
115
|
+
when ExternalAccount::Credentials::CREDENTIAL_TYPE_NAME
|
116
|
+
ExternalAccount::Credentials
|
117
|
+
else
|
118
|
+
raise InitializationError, "credentials type '#{type}' is not supported"
|
119
|
+
end
|
120
|
+
[json_key, clz]
|
89
121
|
end
|
90
122
|
end
|
91
123
|
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "signet/oauth_2/client"
|
4
|
+
|
5
|
+
# Copyright 2025 Google LLC
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
|
19
|
+
module Google
|
20
|
+
module Auth
|
21
|
+
##
|
22
|
+
# Error mixin module for Google Auth errors
|
23
|
+
# All Google Auth errors should include this module
|
24
|
+
#
|
25
|
+
module Error; end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Mixin module that contains detailed error information
|
29
|
+
# typically this is available if credentials initialization
|
30
|
+
# succeeds and credentials object is valid
|
31
|
+
#
|
32
|
+
module DetailedError
|
33
|
+
include Error
|
34
|
+
|
35
|
+
# The type of the credentials that the error was originated from
|
36
|
+
# @return [String, nil] The class name of the credential that raised the error
|
37
|
+
attr_reader :credential_type_name
|
38
|
+
|
39
|
+
# The principal for the authentication flow. Typically obtained from credentials
|
40
|
+
# @return [String, Symbol, nil] The principal identifier associated with the credentials
|
41
|
+
attr_reader :principal
|
42
|
+
|
43
|
+
# All details passed in the options hash when creating the error
|
44
|
+
# @return [Hash] Additional details about the error
|
45
|
+
attr_reader :details
|
46
|
+
|
47
|
+
# @private
|
48
|
+
def self.included base
|
49
|
+
base.extend ClassMethods
|
50
|
+
end
|
51
|
+
|
52
|
+
# Class methods to be added to including classes
|
53
|
+
module ClassMethods
|
54
|
+
# Creates a new error with detailed information
|
55
|
+
# @param message [String] The error message
|
56
|
+
# @param credential_type_name [String] The credential type that raised the error
|
57
|
+
# @param principal [String, Symbol] The principal for the authentication flow
|
58
|
+
# @return [Error] The new error with details
|
59
|
+
def with_details message, credential_type_name:, principal:
|
60
|
+
new(message).tap do |error|
|
61
|
+
error.instance_variable_set :@credential_type_name, credential_type_name
|
62
|
+
error.instance_variable_set :@principal, principal
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Error raised during Credentials initialization.
|
70
|
+
# All new code should use this instead of ArgumentError during initializtion.
|
71
|
+
#
|
72
|
+
class InitializationError < StandardError
|
73
|
+
include Error
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Generic error raised during operation of Credentials
|
78
|
+
# This should be used for all purposes not covered by other errors.
|
79
|
+
#
|
80
|
+
class CredentialsError < StandardError
|
81
|
+
include DetailedError
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# An error indicating the remote server refused to authorize the client.
|
86
|
+
# Maintains backward compatibility with Signet.
|
87
|
+
#
|
88
|
+
# Should not be used in the new code, even when wrapping `Signet::AuthorizationError`.
|
89
|
+
# New code should use CredentialsError instead.
|
90
|
+
#
|
91
|
+
class AuthorizationError < Signet::AuthorizationError
|
92
|
+
include DetailedError
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# An error indicating that the server sent an unexpected http status.
|
97
|
+
# Maintains backward compatibility with Signet.
|
98
|
+
#
|
99
|
+
# Should not be used in the new code, even when wrapping `Signet::UnexpectedStatusError`.
|
100
|
+
# New code should use CredentialsError instead.
|
101
|
+
#
|
102
|
+
class UnexpectedStatusError < Signet::UnexpectedStatusError
|
103
|
+
include DetailedError
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# An error indicating the client failed to parse a value.
|
108
|
+
# Maintains backward compatibility with Signet.
|
109
|
+
#
|
110
|
+
# Should not be used in the new code, even when wrapping `Signet::ParseError`.
|
111
|
+
# New code should use CredentialsError instead.
|
112
|
+
#
|
113
|
+
class ParseError < Signet::ParseError
|
114
|
+
include DetailedError
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -13,6 +13,7 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
15
|
require "time"
|
16
|
+
require "googleauth/errors"
|
16
17
|
require "googleauth/external_account/base_credentials"
|
17
18
|
require "googleauth/external_account/external_account_utils"
|
18
19
|
|
@@ -106,17 +107,32 @@ module Google
|
|
106
107
|
|
107
108
|
private
|
108
109
|
|
110
|
+
# Retrieves an IMDSv2 session token or returns a cached token if valid
|
111
|
+
#
|
112
|
+
# @return [String] The IMDSv2 session token
|
113
|
+
# @raise [Google::Auth::CredentialsError] If the token URL is missing or there's an error retrieving the token
|
109
114
|
def imdsv2_session_token
|
110
115
|
return @imdsv2_session_token unless imdsv2_session_token_invalid?
|
111
|
-
|
116
|
+
if @imdsv2_session_token_url.nil?
|
117
|
+
raise CredentialsError.with_details(
|
118
|
+
"IMDSV2 token url must be provided",
|
119
|
+
credential_type_name: self.class.name,
|
120
|
+
principal: principal
|
121
|
+
)
|
122
|
+
end
|
112
123
|
begin
|
113
124
|
response = connection.put @imdsv2_session_token_url do |req|
|
114
125
|
req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
|
115
126
|
end
|
127
|
+
raise Faraday::Error unless response.success?
|
116
128
|
rescue Faraday::Error => e
|
117
|
-
raise
|
129
|
+
raise CredentialsError.with_details(
|
130
|
+
"Fetching AWS IMDSV2 token error: #{e}",
|
131
|
+
credential_type_name: self.class.name,
|
132
|
+
principal: principal
|
133
|
+
)
|
118
134
|
end
|
119
|
-
|
135
|
+
|
120
136
|
@imdsv2_session_token = response.body
|
121
137
|
@imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS
|
122
138
|
@imdsv2_session_token
|
@@ -127,6 +143,14 @@ module Google
|
|
127
143
|
@imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now
|
128
144
|
end
|
129
145
|
|
146
|
+
# Makes a request to an AWS resource endpoint
|
147
|
+
#
|
148
|
+
# @param [String] url The AWS endpoint URL
|
149
|
+
# @param [String] name Resource name for error messages
|
150
|
+
# @param [Hash, nil] data Optional data to send in POST requests
|
151
|
+
# @param [Hash] headers Optional request headers
|
152
|
+
# @return [Faraday::Response] The successful response
|
153
|
+
# @raise [Google::Auth::CredentialsError] If the request fails
|
130
154
|
def get_aws_resource url, name, data: nil, headers: {}
|
131
155
|
begin
|
132
156
|
headers["x-aws-ec2-metadata-token"] = imdsv2_session_token
|
@@ -136,11 +160,14 @@ module Google
|
|
136
160
|
else
|
137
161
|
connection.get url, nil, headers
|
138
162
|
end
|
139
|
-
|
140
163
|
raise Faraday::Error unless response.success?
|
141
164
|
response
|
142
165
|
rescue Faraday::Error
|
143
|
-
raise
|
166
|
+
raise CredentialsError.with_details(
|
167
|
+
"Failed to retrieve AWS #{name}.",
|
168
|
+
credential_type_name: self.class.name,
|
169
|
+
principal: principal
|
170
|
+
)
|
144
171
|
end
|
145
172
|
end
|
146
173
|
|
@@ -181,9 +208,16 @@ module Google
|
|
181
208
|
# Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server.
|
182
209
|
# This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security
|
183
210
|
# credentials needed to sign requests to AWS APIs.
|
211
|
+
#
|
212
|
+
# @return [String] The AWS role name
|
213
|
+
# @raise [Google::Auth::CredentialsError] If the credential verification URL is not set or if the request fails
|
184
214
|
def fetch_metadata_role_name
|
185
215
|
unless @credential_verification_url
|
186
|
-
raise
|
216
|
+
raise CredentialsError.with_details(
|
217
|
+
"Unable to determine the AWS metadata server security credentials endpoint",
|
218
|
+
credential_type_name: self.class.name,
|
219
|
+
principal: principal
|
220
|
+
)
|
187
221
|
end
|
188
222
|
|
189
223
|
get_aws_resource(@credential_verification_url, "IAM Role").body
|
@@ -195,11 +229,22 @@ module Google
|
|
195
229
|
MultiJson.load response.body
|
196
230
|
end
|
197
231
|
|
232
|
+
# Reads the name of the AWS region from the environment
|
233
|
+
#
|
234
|
+
# @return [String] The name of the AWS region
|
235
|
+
# @raise [Google::Auth::CredentialsError] If the region is not set in the environment
|
236
|
+
# and the region_url was not set in credentials source
|
198
237
|
def region
|
199
238
|
@region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]
|
200
239
|
|
201
240
|
unless @region
|
202
|
-
|
241
|
+
unless @region_url
|
242
|
+
raise CredentialsError.with_details(
|
243
|
+
"region_url or region must be set for external account credentials",
|
244
|
+
credential_type_name: self.class.name,
|
245
|
+
principal: principal
|
246
|
+
)
|
247
|
+
end
|
203
248
|
|
204
249
|
@region ||= get_aws_resource(@region_url, "region").body[0..-2]
|
205
250
|
end
|
@@ -220,23 +265,45 @@ module Google
|
|
220
265
|
@region_name = region_name
|
221
266
|
end
|
222
267
|
|
223
|
-
# Generates
|
224
|
-
#
|
268
|
+
# Generates an AWS signature version 4 signed request.
|
269
|
+
#
|
270
|
+
# Creates a signed request following the AWS Signature Version 4 process, which
|
271
|
+
# provides secure authentication for AWS API calls. The process includes creating
|
272
|
+
# canonical request strings, calculating signatures using the AWS credentials, and
|
273
|
+
# building proper authorization headers.
|
274
|
+
#
|
275
|
+
# For detailed information on the signing process, see:
|
225
276
|
# https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
226
277
|
#
|
227
|
-
# @param [Hash
|
228
|
-
#
|
229
|
-
#
|
230
|
-
#
|
231
|
-
# @param [
|
232
|
-
#
|
278
|
+
# @param [Hash] aws_credentials The AWS security credentials with the following keys:
|
279
|
+
# @option aws_credentials [String] :access_key_id The AWS access key ID
|
280
|
+
# @option aws_credentials [String] :secret_access_key The AWS secret access key
|
281
|
+
# @option aws_credentials [String, nil] :session_token Optional AWS session token
|
282
|
+
# @param [Hash] original_request The request to sign with the following keys:
|
283
|
+
# @option original_request [String] :url The AWS service URL (must be HTTPS)
|
284
|
+
# @option original_request [String] :method The HTTP method (GET, POST, etc.)
|
285
|
+
# @option original_request [Hash, nil] :headers Optional request headers
|
286
|
+
# @option original_request [String, nil] :data Optional request payload
|
287
|
+
#
|
288
|
+
# @return [Hash] The signed request with the following keys:
|
289
|
+
# * :url - The original URL as a string
|
290
|
+
# * :headers - A hash of headers with the authorization header added
|
291
|
+
# * :method - The HTTP method
|
292
|
+
# * :data - The request payload (if present)
|
233
293
|
#
|
234
|
-
# @
|
235
|
-
# The AWS signed request dictionary object.
|
294
|
+
# @raise [Google::Auth::CredentialsError] If the AWS service URL is invalid
|
236
295
|
#
|
237
296
|
def generate_signed_request aws_credentials, original_request
|
238
297
|
uri = Addressable::URI.parse original_request[:url]
|
239
|
-
|
298
|
+
unless uri.hostname && uri.scheme == "https"
|
299
|
+
# NOTE: We use AwsCredentials name but can't access its principal since AwsRequestSigner
|
300
|
+
# is a separate class and not a credential object with access to the audience
|
301
|
+
raise CredentialsError.with_details(
|
302
|
+
"Invalid AWS service URL",
|
303
|
+
credential_type_name: AwsCredentials.name,
|
304
|
+
principal: "aws"
|
305
|
+
)
|
306
|
+
end
|
240
307
|
service_name = uri.host.split(".").first
|
241
308
|
|
242
309
|
datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
|