googleauth 1.12.2 → 1.13.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 +15 -0
- data/README.md +9 -0
- data/lib/googleauth/compute_engine.rb +39 -0
- data/lib/googleauth/credentials.rb +107 -25
- data/lib/googleauth/default_credentials.rb +12 -1
- data/lib/googleauth/impersonated_service_account.rb +282 -0
- data/lib/googleauth/service_account.rb +102 -6
- data/lib/googleauth/signet.rb +52 -0
- data/lib/googleauth/user_refresh.rb +43 -0
- data/lib/googleauth/version.rb +1 -1
- metadata +4 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d8ca5b2b0c7f4ce54f7971d8de2f23f3ee0837d08d7d3c568c503308fcf82ab
|
4
|
+
data.tar.gz: d5f8b8fd2fcb4fef4240db58bf90f54a8bfd021c550a7bc9063c9087285f3921
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a4de2b23f4dc0310a18568e0618c5d81fad54dfc8d57fe3c27b954c4bd21272fcc467c2c313f98f80fa127eab11ebe0d0fc55ceed7e6c5439764500e334df49
|
7
|
+
data.tar.gz: f4aff68138105ea19875bb7a51d4b6ced9ec2e7185ce725eca205ea1dc11beb9e6019c9dd89449866598ca6e4d3abb06b91f277f34e51ac7726d79a39fc40c67
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Release History
|
2
2
|
|
3
|
+
### 1.13.1 (2025-01-24)
|
4
|
+
|
5
|
+
#### Bug Fixes
|
6
|
+
|
7
|
+
* Signet client subclasses no longer make the update! method private ([#516](https://github.com/googleapis/google-auth-library-ruby/issues/516))
|
8
|
+
|
9
|
+
### 1.13.0 (2025-01-22)
|
10
|
+
|
11
|
+
#### Features
|
12
|
+
|
13
|
+
* create impersonated service credentials ([#499](https://github.com/googleapis/google-auth-library-ruby/issues/499))
|
14
|
+
#### Documentation
|
15
|
+
|
16
|
+
* Include note about validating externally-provided credentials ([#512](https://github.com/googleapis/google-auth-library-ruby/issues/512))
|
17
|
+
|
3
18
|
### 1.12.2 (2024-12-19)
|
4
19
|
|
5
20
|
#### Bug Fixes
|
data/README.md
CHANGED
@@ -64,6 +64,15 @@ well as a web variant tailored toward Rack-based applications.
|
|
64
64
|
The authorizers are intended for authorization use cases. For sign-on,
|
65
65
|
see [Google Identity Platform](https://developers.google.com/identity/)
|
66
66
|
|
67
|
+
## Important notes
|
68
|
+
|
69
|
+
If you accept a credential configuration (credential JSON/File/Stream) from an
|
70
|
+
external source for authentication to Google Cloud, you must validate it before
|
71
|
+
providing it to any Google API or library. Providing an unvalidated credential
|
72
|
+
configuration to Google APIs can compromise the security of your systems and data.
|
73
|
+
For more information, refer to [Validate credential configurations from external
|
74
|
+
sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
75
|
+
|
67
76
|
### Example (Web)
|
68
77
|
|
69
78
|
```ruby
|
@@ -93,6 +93,24 @@ module Google
|
|
93
93
|
super options
|
94
94
|
end
|
95
95
|
|
96
|
+
# Creates a duplicate of these credentials
|
97
|
+
# without the Signet::OAuth2::Client-specific
|
98
|
+
# transient state (e.g. cached tokens)
|
99
|
+
#
|
100
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
101
|
+
# The following keys are recognized in addition to keys in the
|
102
|
+
# Signet::OAuth2::Client
|
103
|
+
# * `:universe_domain_overridden` Whether the universe domain was
|
104
|
+
# overriden during credentials creation
|
105
|
+
def duplicate options = {}
|
106
|
+
options = deep_hash_normalize options
|
107
|
+
super(
|
108
|
+
{
|
109
|
+
universe_domain_overridden: @universe_domain_overridden
|
110
|
+
}.merge(options)
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
96
114
|
# @private
|
97
115
|
# Overrides universe_domain getter to fetch lazily if it hasn't been
|
98
116
|
# fetched yet. This is necessary specifically for Compute Engine because
|
@@ -136,6 +154,27 @@ module Google
|
|
136
154
|
end
|
137
155
|
end
|
138
156
|
|
157
|
+
# Destructively updates these credentials.
|
158
|
+
#
|
159
|
+
# This method is called by `Signet::OAuth2::Client`'s constructor
|
160
|
+
#
|
161
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
162
|
+
# The following keys are recognized in addition to keys in the
|
163
|
+
# Signet::OAuth2::Client
|
164
|
+
# * `:universe_domain_overridden` Whether the universe domain was
|
165
|
+
# overriden during credentials creation
|
166
|
+
# @return [Google::Auth::GCECredentials]
|
167
|
+
def update! options = {}
|
168
|
+
# Normalize all keys to symbols to allow indifferent access.
|
169
|
+
options = deep_hash_normalize options
|
170
|
+
|
171
|
+
@universe_domain_overridden = options[:universe_domain_overridden] if options.key? :universe_domain_overridden
|
172
|
+
|
173
|
+
super(options)
|
174
|
+
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
139
178
|
private
|
140
179
|
|
141
180
|
def log_fetch_query
|
@@ -26,6 +26,14 @@ module Google
|
|
26
26
|
# In most cases, it is subclassed by API-specific credential classes that
|
27
27
|
# can be instantiated by clients.
|
28
28
|
#
|
29
|
+
# **Important:** If you accept a credential configuration (credential
|
30
|
+
# JSON/File/Stream) from an external source for authentication to Google
|
31
|
+
# Cloud, you must validate it before providing it to any Google API or
|
32
|
+
# library. Providing an unvalidated credential configuration to Google APIs
|
33
|
+
# can compromise the security of your systems and data. For more
|
34
|
+
# information, refer to [Validate credential configurations from external
|
35
|
+
# sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
36
|
+
#
|
29
37
|
# ## Options
|
30
38
|
#
|
31
39
|
# Credentials classes are configured with options that dictate default
|
@@ -321,9 +329,6 @@ module Google
|
|
321
329
|
# @return [String, Array<String>] The scope for this client. A scope is an access range
|
322
330
|
# defined by the authorization server. The scope can be a single value or a list of values.
|
323
331
|
#
|
324
|
-
# @!attribute [r] target_audience
|
325
|
-
# @return [String] The final target audience for ID tokens returned by this credential.
|
326
|
-
#
|
327
332
|
# @!attribute [r] issuer
|
328
333
|
# @return [String] The issuer ID associated with this client.
|
329
334
|
#
|
@@ -334,6 +339,9 @@ module Google
|
|
334
339
|
# @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
|
335
340
|
# suitable for passing as a closure.
|
336
341
|
#
|
342
|
+
# @!attribute [r] target_audience
|
343
|
+
# @return [String] The final target audience for ID tokens returned by this credential.
|
344
|
+
#
|
337
345
|
# @!attribute [rw] universe_domain
|
338
346
|
# @return [String] The universe domain issuing these credentials.
|
339
347
|
#
|
@@ -349,33 +357,53 @@ module Google
|
|
349
357
|
# Creates a new Credentials instance with the provided auth credentials, and with the default
|
350
358
|
# values configured on the class.
|
351
359
|
#
|
352
|
-
# @param [String, Hash, Signet::OAuth2::Client]
|
353
|
-
# The
|
360
|
+
# @param [String, Hash, Signet::OAuth2::Client] source_creds
|
361
|
+
# The source of credentials. It can be provided as one of the following:
|
354
362
|
#
|
355
363
|
# * The path to a JSON keyfile (as a `String`)
|
356
364
|
# * The contents of a JSON keyfile (as a `Hash`)
|
357
|
-
# * A `Signet::OAuth2::Client` object
|
365
|
+
# * A `Signet::OAuth2::Client` credentials object
|
366
|
+
# * Any credentials object that supports the methods this wrapper delegates to an inner client.
|
367
|
+
#
|
368
|
+
# If this parameter is an object (`Signet::OAuth2::Client` or other) it will be used as an inner client.
|
369
|
+
# Otherwise the inner client will be constructed from the JSON keyfile or the contens of the hash.
|
370
|
+
#
|
358
371
|
# @param [Hash] options
|
359
|
-
# The options for configuring
|
372
|
+
# The options for configuring this wrapper credentials object and the inner client.
|
373
|
+
# The options hash is used in two ways:
|
360
374
|
#
|
361
|
-
#
|
362
|
-
#
|
363
|
-
# * `:connection_builder` - the connection builder to use for the client
|
364
|
-
# * `:default_connection` - the default connection to use for the client
|
365
|
-
# * `:logger` - the logger used to log credential operations such as token refresh.
|
375
|
+
# 1. **Configuring the wrapper object:** Some options are used to directly
|
376
|
+
# configure the wrapper `Credentials` instance. These include:
|
366
377
|
#
|
367
|
-
|
368
|
-
|
378
|
+
# * `:project_id` (and optionally `:project`) - the project identifier for the client
|
379
|
+
# * `:quota_project_id` - the quota project identifier for the client
|
380
|
+
# * `:logger` - the logger used to log credential operations such as token refresh.
|
381
|
+
#
|
382
|
+
# 2. **Configuring the inner client:** When the `source_creds` parameter
|
383
|
+
# is a `String` or `Hash`, a new `Signet::OAuth2::Client` is created
|
384
|
+
# internally. The following options are used to configure this inner client:
|
385
|
+
#
|
386
|
+
# * `:scope` - the scope for the client
|
387
|
+
# * `:target_audience` - the target audience for the client
|
388
|
+
#
|
389
|
+
# Any other options in the `options` hash are passed directly to the
|
390
|
+
# inner client constructor. This allows you to configure additional
|
391
|
+
# parameters of the `Signet::OAuth2::Client`, such as connection parameters,
|
392
|
+
# timeouts, etc.
|
393
|
+
#
|
394
|
+
def initialize source_creds, options = {}
|
395
|
+
raise "The source credentials passed to Google::Auth::Credentials.new were nil." if source_creds.nil?
|
396
|
+
|
369
397
|
options = symbolize_hash_keys options
|
370
398
|
@project_id = options[:project_id] || options[:project]
|
371
399
|
@quota_project_id = options[:quota_project_id]
|
372
|
-
case
|
373
|
-
when
|
374
|
-
|
400
|
+
case source_creds
|
401
|
+
when String
|
402
|
+
update_from_filepath source_creds, options
|
375
403
|
when Hash
|
376
|
-
update_from_hash
|
404
|
+
update_from_hash source_creds, options
|
377
405
|
else
|
378
|
-
|
406
|
+
update_from_client source_creds
|
379
407
|
end
|
380
408
|
setup_logging logger: options.fetch(:logger, :default)
|
381
409
|
@project_id ||= CredentialsLoader.load_gcloud_project_id
|
@@ -481,14 +509,50 @@ module Google
|
|
481
509
|
:from_application_default,
|
482
510
|
:from_io
|
483
511
|
|
484
|
-
protected
|
485
512
|
|
486
|
-
#
|
487
|
-
|
488
|
-
|
489
|
-
|
513
|
+
# Creates a duplicate of these credentials. This method tries to create the duplicate of the
|
514
|
+
# wrapped credentials if they support duplication and use them as is if they don't.
|
515
|
+
#
|
516
|
+
# The wrapped credentials are typically `Signet::OAuth2::Client` objects and they keep
|
517
|
+
# the transient state (token, refresh token, etc). The duplication discards that state,
|
518
|
+
# allowing e.g. to get the token with a different scope.
|
519
|
+
#
|
520
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
521
|
+
#
|
522
|
+
# The options hash is used in two ways:
|
523
|
+
#
|
524
|
+
# 1. **Configuring the duplicate of the wrapper object:** Some options are used to directly
|
525
|
+
# configure the wrapper `Credentials` instance. These include:
|
526
|
+
#
|
527
|
+
# * `:project_id` (and optionally `:project`) - the project identifier for the credentials
|
528
|
+
# * `:quota_project_id` - the quota project identifier for the credentials
|
529
|
+
#
|
530
|
+
# 2. **Configuring the duplicate of the inner client:** If the inner client supports duplication
|
531
|
+
# the options hash is passed to it. This allows for configuration of additional parameters,
|
532
|
+
# most importantly (but not limited to) the following:
|
533
|
+
#
|
534
|
+
# * `:scope` - the scope for the client
|
535
|
+
#
|
536
|
+
# @return [Credentials]
|
537
|
+
def duplicate options = {}
|
538
|
+
options = deep_hash_normalize options
|
539
|
+
|
540
|
+
options = {
|
541
|
+
project_id: @project_id,
|
542
|
+
quota_project_id: @quota_project_id
|
543
|
+
}.merge(options)
|
544
|
+
|
545
|
+
new_client = if @client.respond_to? :duplicate
|
546
|
+
@client.duplicate options
|
547
|
+
else
|
548
|
+
@client
|
549
|
+
end
|
550
|
+
|
551
|
+
self.class.new new_client, options
|
490
552
|
end
|
491
553
|
|
554
|
+
protected
|
555
|
+
|
492
556
|
# Verify that the keyfile argument is a file.
|
493
557
|
def verify_keyfile_exists! keyfile
|
494
558
|
exists = ::File.file? keyfile
|
@@ -530,11 +594,12 @@ module Google
|
|
530
594
|
options
|
531
595
|
end
|
532
596
|
|
533
|
-
def
|
597
|
+
def update_from_client client
|
534
598
|
@project_id ||= client.project_id if client.respond_to? :project_id
|
535
599
|
@quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
|
536
600
|
@client = client
|
537
601
|
end
|
602
|
+
alias update_from_signet update_from_client
|
538
603
|
|
539
604
|
def update_from_hash hash, options
|
540
605
|
hash = stringify_hash_keys hash
|
@@ -571,6 +636,23 @@ module Google
|
|
571
636
|
end
|
572
637
|
@client.logger = logger
|
573
638
|
end
|
639
|
+
|
640
|
+
private
|
641
|
+
|
642
|
+
# Convert all keys in this hash (nested) to symbols for uniform retrieval
|
643
|
+
def recursive_hash_normalize_keys val
|
644
|
+
if val.is_a? Hash
|
645
|
+
deep_hash_normalize val
|
646
|
+
else
|
647
|
+
val
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
def deep_hash_normalize old_hash
|
652
|
+
sym_hash = {}
|
653
|
+
old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
|
654
|
+
sym_hash
|
655
|
+
end
|
574
656
|
end
|
575
657
|
end
|
576
658
|
end
|
@@ -19,6 +19,7 @@ require "googleauth/credentials_loader"
|
|
19
19
|
require "googleauth/service_account"
|
20
20
|
require "googleauth/user_refresh"
|
21
21
|
require "googleauth/external_account"
|
22
|
+
require "googleauth/impersonated_service_account"
|
22
23
|
|
23
24
|
module Google
|
24
25
|
# Module Auth provides classes that provide Google-specific authorization
|
@@ -29,8 +30,18 @@ module Google
|
|
29
30
|
class DefaultCredentials
|
30
31
|
extend CredentialsLoader
|
31
32
|
|
32
|
-
|
33
|
+
##
|
34
|
+
# Override CredentialsLoader#make_creds to use the class determined by
|
33
35
|
# loading the json.
|
36
|
+
#
|
37
|
+
# **Important:** If you accept a credential configuration (credential
|
38
|
+
# JSON/File/Stream) from an external source for authentication to Google
|
39
|
+
# Cloud, you must validate it before providing it to any Google API or
|
40
|
+
# library. Providing an unvalidated credential configuration to Google
|
41
|
+
# APIs can compromise the security of your systems and data. For more
|
42
|
+
# information, refer to [Validate credential configurations from external
|
43
|
+
# sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
44
|
+
#
|
34
45
|
def self.make_creds options = {}
|
35
46
|
json_key_io = options[:json_key_io]
|
36
47
|
if json_key_io
|
@@ -0,0 +1,282 @@
|
|
1
|
+
# Copyright 2024 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 "googleauth/signet"
|
16
|
+
require "googleauth/base_client"
|
17
|
+
require "googleauth/helpers/connection"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
module Auth
|
21
|
+
# Authenticates requests using impersonation from base credentials.
|
22
|
+
# This is a two-step process: first authentication claim from the base credentials is created
|
23
|
+
# and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint.
|
24
|
+
# The short-lived token and its expiration time are cached.
|
25
|
+
class ImpersonatedServiceAccountCredentials
|
26
|
+
# @private
|
27
|
+
ERROR_SUFFIX = <<~ERROR.freeze
|
28
|
+
when trying to get security access token
|
29
|
+
from IAM Credentials endpoint using the credentials provided.
|
30
|
+
ERROR
|
31
|
+
|
32
|
+
# @private
|
33
|
+
IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
|
34
|
+
|
35
|
+
# BaseClient most importantly implements the `:updater_proc` getter,
|
36
|
+
# that returns a reference to an `apply!` method that updates
|
37
|
+
# a hash argument provided with the authorization header containing
|
38
|
+
# the access token (impersonation token in this case).
|
39
|
+
include Google::Auth::BaseClient
|
40
|
+
|
41
|
+
include Helpers::Connection
|
42
|
+
|
43
|
+
# @return [Object] The original authenticated credentials used to fetch short-lived impersonation access tokens
|
44
|
+
attr_reader :base_credentials
|
45
|
+
|
46
|
+
# @return [Object] The modified version of base credentials, tailored for impersonation purposes
|
47
|
+
# with necessary scope adjustments
|
48
|
+
attr_reader :source_credentials
|
49
|
+
|
50
|
+
# @return [String] The URL endpoint used to generate an impersonation token. This URL should follow a specific
|
51
|
+
# format to specify the impersonated service account.
|
52
|
+
attr_reader :impersonation_url
|
53
|
+
|
54
|
+
# @return [Array<String>, String] The scope(s) required for the impersonated access token,
|
55
|
+
# indicating the permissions needed for the short-lived token
|
56
|
+
attr_reader :scope
|
57
|
+
|
58
|
+
# @return [String, nil] The short-lived impersonation access token, retrieved and cached
|
59
|
+
# after making the impersonation request
|
60
|
+
attr_reader :access_token
|
61
|
+
|
62
|
+
# @return [Time, nil] The expiration time of the current access token, used to determine
|
63
|
+
# if the token is still valid
|
64
|
+
attr_reader :expires_at
|
65
|
+
|
66
|
+
# Create a ImpersonatedServiceAccountCredentials
|
67
|
+
# When you use service account impersonation, you start with an authenticated principal
|
68
|
+
# (e.g. your user account or a service account)
|
69
|
+
# and request short-lived credentials for a service account
|
70
|
+
# that has the authorization that your use case requires.
|
71
|
+
#
|
72
|
+
# @param options [Hash] A hash of options to configure the credentials.
|
73
|
+
# @option options [Object] :base_credentials (required) The authenticated principal.
|
74
|
+
# It will be used as following:
|
75
|
+
# * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
|
76
|
+
# * as source credentials otherwise.
|
77
|
+
# @option options [String] :impersonation_url (required) The URL to impersonate the service account.
|
78
|
+
# This URL should follow the format:
|
79
|
+
# `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
|
80
|
+
# where:
|
81
|
+
# - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
|
82
|
+
# - `{source_sa_email}` is the email address of the service account to impersonate.
|
83
|
+
# @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
|
84
|
+
# defining the permissions required for the token.
|
85
|
+
# @option options [Object] :source_credentials The authenticated principal that will be used
|
86
|
+
# to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
|
87
|
+
#
|
88
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
89
|
+
def self.make_creds options = {}
|
90
|
+
new options
|
91
|
+
end
|
92
|
+
|
93
|
+
# Initializes a new instance of ImpersonatedServiceAccountCredentials.
|
94
|
+
#
|
95
|
+
# @param options [Hash] A hash of options to configure the credentials.
|
96
|
+
# @option options [Object] :base_credentials (required) The authenticated principal.
|
97
|
+
# It will be used as following:
|
98
|
+
# * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
|
99
|
+
# * as source credentials otherwise.
|
100
|
+
# @option options [String] :impersonation_url (required) The URL to impersonate the service account.
|
101
|
+
# This URL should follow the format:
|
102
|
+
# `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
|
103
|
+
# where:
|
104
|
+
# - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
|
105
|
+
# - `{source_sa_email}` is the email address of the service account to impersonate.
|
106
|
+
# @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
|
107
|
+
# defining the permissions required for the token.
|
108
|
+
# @option options [Object] :source_credentials The authenticated principal that will be used
|
109
|
+
# to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
|
110
|
+
# It is redundant to provide both source and base credentials as only source will be used,
|
111
|
+
# but it can be done, e.g. when duplicating existing credentials.
|
112
|
+
#
|
113
|
+
# @raise [ArgumentError] If any of the required options are missing.
|
114
|
+
#
|
115
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
116
|
+
def initialize options = {}
|
117
|
+
@base_credentials, @impersonation_url, @scope =
|
118
|
+
options.values_at :base_credentials,
|
119
|
+
:impersonation_url,
|
120
|
+
:scope
|
121
|
+
|
122
|
+
# Fail-fast checks for required parameters
|
123
|
+
if @base_credentials.nil? && !options.key?(:source_credentials)
|
124
|
+
raise ArgumentError, "Missing required option: either :base_credentials or :source_credentials"
|
125
|
+
end
|
126
|
+
raise ArgumentError, "Missing required option: :impersonation_url" if @impersonation_url.nil?
|
127
|
+
raise ArgumentError, "Missing required option: :scope" if @scope.nil?
|
128
|
+
|
129
|
+
# Some credentials (all Signet-based ones and this one) include scope and a bunch of transient state
|
130
|
+
# (e.g. refresh status) as part of themselves
|
131
|
+
# so a copy needs to be created with the scope overriden and transient state dropped.
|
132
|
+
#
|
133
|
+
# If a credentials does not support `duplicate` we'll try to use it as is assuming it has a broad enough scope.
|
134
|
+
# This might result in an "access denied" error downstream when the token from that credentials is being used
|
135
|
+
# for the token exchange.
|
136
|
+
@source_credentials = if options.key? :source_credentials
|
137
|
+
options[:source_credentials]
|
138
|
+
elsif @base_credentials.respond_to? :duplicate
|
139
|
+
@base_credentials.duplicate({
|
140
|
+
scope: IAM_SCOPE
|
141
|
+
})
|
142
|
+
else
|
143
|
+
@base_credentials
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Determines whether the current access token expires within the specified number of seconds.
|
148
|
+
#
|
149
|
+
# @param seconds [Integer] The number of seconds to check against the token's expiration time.
|
150
|
+
#
|
151
|
+
# @return [Boolean] Whether the access token expires within the given time frame
|
152
|
+
def expires_within? seconds
|
153
|
+
# This method is needed for BaseClient
|
154
|
+
@expires_at && @expires_at - Time.now.utc < seconds
|
155
|
+
end
|
156
|
+
|
157
|
+
# The universe domain of the impersonated credentials.
|
158
|
+
# Effectively this retrieves the universe domain of the source credentials.
|
159
|
+
#
|
160
|
+
# @return [String] The universe domain of the credentials.
|
161
|
+
def universe_domain
|
162
|
+
@source_credentials.universe_domain
|
163
|
+
end
|
164
|
+
|
165
|
+
# @return [Logger, nil] The logger of the credentials.
|
166
|
+
def logger
|
167
|
+
@source_credentials.logger if source_credentials.respond_to? :logger
|
168
|
+
end
|
169
|
+
|
170
|
+
# Creates a duplicate of these credentials without transient token state
|
171
|
+
#
|
172
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
173
|
+
# The following keys are recognized
|
174
|
+
# * `base_credentials` the base credentials used to initialize the impersonation
|
175
|
+
# * `source_credentials` the authenticated credentials which usually would be
|
176
|
+
# base credentials with scope overridden to IAM_SCOPE
|
177
|
+
# * `impersonation_url` the URL to use to make an impersonation token exchange
|
178
|
+
# * `scope` the scope(s) to access
|
179
|
+
#
|
180
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
181
|
+
def duplicate options = {}
|
182
|
+
options = deep_hash_normalize options
|
183
|
+
|
184
|
+
options = {
|
185
|
+
base_credentials: @base_credentials,
|
186
|
+
source_credentials: @source_credentials,
|
187
|
+
impersonation_url: @impersonation_url,
|
188
|
+
scope: @scope
|
189
|
+
}.merge(options)
|
190
|
+
|
191
|
+
self.class.new options
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
# Generates a new impersonation access token by exchanging the source credentials' token
|
197
|
+
# at the impersonation URL.
|
198
|
+
#
|
199
|
+
# This method first fetches an access token from the source credentials and then exchanges it
|
200
|
+
# for an impersonation token using the specified impersonation URL. The generated token and
|
201
|
+
# its expiration time are cached for subsequent use.
|
202
|
+
#
|
203
|
+
# @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
|
204
|
+
#
|
205
|
+
# @raise [Signet::UnexpectedStatusError] If the response status is 403 or 500.
|
206
|
+
# @raise [Signet::AuthorizationError] For other unexpected response statuses.
|
207
|
+
#
|
208
|
+
# @return [String] The newly generated impersonation access token.
|
209
|
+
def fetch_access_token! _options = {}
|
210
|
+
auth_header = {}
|
211
|
+
auth_header = @source_credentials.updater_proc.call auth_header
|
212
|
+
|
213
|
+
resp = connection.post @impersonation_url do |req|
|
214
|
+
req.headers.merge! auth_header
|
215
|
+
req.headers["Content-Type"] = "application/json"
|
216
|
+
req.body = MultiJson.dump({ scope: @scope })
|
217
|
+
end
|
218
|
+
|
219
|
+
case resp.status
|
220
|
+
when 200
|
221
|
+
response = MultiJson.load resp.body
|
222
|
+
self.expires_at = response["expireTime"]
|
223
|
+
@access_token = response["accessToken"]
|
224
|
+
access_token
|
225
|
+
when 403, 500
|
226
|
+
msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
|
227
|
+
raise Signet::UnexpectedStatusError, msg
|
228
|
+
else
|
229
|
+
msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
|
230
|
+
raise Signet::AuthorizationError, msg
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Setter for the expires_at value that makes sure it is converted
|
235
|
+
# to Time object.
|
236
|
+
def expires_at= new_expires_at
|
237
|
+
@expires_at = normalize_timestamp new_expires_at
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns the type of token (access_token).
|
241
|
+
# This method is needed for BaseClient.
|
242
|
+
def token_type
|
243
|
+
:access_token
|
244
|
+
end
|
245
|
+
|
246
|
+
# Normalizes a timestamp to a Time object.
|
247
|
+
#
|
248
|
+
# @param time [Time, String, nil] The timestamp to normalize.
|
249
|
+
#
|
250
|
+
# @return [Time, nil] The normalized Time object, or nil if the input is nil.
|
251
|
+
#
|
252
|
+
# @raise [RuntimeError] If the input is not a Time, String, or nil.
|
253
|
+
def normalize_timestamp time
|
254
|
+
case time
|
255
|
+
when NilClass
|
256
|
+
nil
|
257
|
+
when Time
|
258
|
+
time
|
259
|
+
when String
|
260
|
+
Time.parse time
|
261
|
+
else
|
262
|
+
raise "Invalid time value #{time}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Convert all keys in this hash (nested) to symbols for uniform retrieval
|
267
|
+
def recursive_hash_normalize_keys val
|
268
|
+
if val.is_a? Hash
|
269
|
+
deep_hash_normalize val
|
270
|
+
else
|
271
|
+
val
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def deep_hash_normalize old_hash
|
276
|
+
sym_hash = {}
|
277
|
+
old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
|
278
|
+
sym_hash
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
@@ -81,6 +81,30 @@ module Google
|
|
81
81
|
.configure_connection(options)
|
82
82
|
end
|
83
83
|
|
84
|
+
# Creates a duplicate of these credentials
|
85
|
+
# without the Signet::OAuth2::Client-specific
|
86
|
+
# transient state (e.g. cached tokens)
|
87
|
+
#
|
88
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
89
|
+
# The following keys are recognized in addition to keys in the
|
90
|
+
# Signet::OAuth2::Client
|
91
|
+
# * `:enable_self_signed_jwt` Whether the self-signed JWT should
|
92
|
+
# be used for the authentication
|
93
|
+
# * `project_id` the project id to use during the authentication
|
94
|
+
# * `quota_project_id` the quota project id to use
|
95
|
+
# during the authentication
|
96
|
+
def duplicate options = {}
|
97
|
+
options = deep_hash_normalize options
|
98
|
+
super(
|
99
|
+
{
|
100
|
+
enable_self_signed_jwt: @enable_self_signed_jwt,
|
101
|
+
project_id: project_id,
|
102
|
+
quota_project_id: quota_project_id,
|
103
|
+
logger: logger
|
104
|
+
}.merge(options)
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
84
108
|
# Handles certain escape sequences that sometimes appear in input.
|
85
109
|
# Specifically, interprets the "\n" sequence for newline, and removes
|
86
110
|
# enclosing quotes.
|
@@ -112,6 +136,32 @@ module Google
|
|
112
136
|
super && !enable_self_signed_jwt?
|
113
137
|
end
|
114
138
|
|
139
|
+
# Destructively updates these credentials
|
140
|
+
#
|
141
|
+
# This method is called by `Signet::OAuth2::Client`'s constructor
|
142
|
+
#
|
143
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
144
|
+
# The following keys are recognized in addition to keys in the
|
145
|
+
# Signet::OAuth2::Client
|
146
|
+
# * `:enable_self_signed_jwt` Whether the self-signed JWT should
|
147
|
+
# be used for the authentication
|
148
|
+
# * `project_id` the project id to use during the authentication
|
149
|
+
# * `quota_project_id` the quota project id to use
|
150
|
+
# during the authentication
|
151
|
+
# @return [Google::Auth::ServiceAccountCredentials]
|
152
|
+
def update! options = {}
|
153
|
+
# Normalize all keys to symbols to allow indifferent access.
|
154
|
+
options = deep_hash_normalize options
|
155
|
+
|
156
|
+
@enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
|
157
|
+
@project_id = options[:project_id] if options.key? :project_id
|
158
|
+
@quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
|
159
|
+
|
160
|
+
super(options)
|
161
|
+
|
162
|
+
self
|
163
|
+
end
|
164
|
+
|
115
165
|
private
|
116
166
|
|
117
167
|
def apply_self_signed_jwt! a_hash
|
@@ -144,8 +194,10 @@ module Google
|
|
144
194
|
TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
|
145
195
|
SIGNING_ALGORITHM = "RS256".freeze
|
146
196
|
EXPIRY = 60
|
197
|
+
|
147
198
|
extend CredentialsLoader
|
148
199
|
extend JsonKeyReader
|
200
|
+
|
149
201
|
attr_reader :project_id
|
150
202
|
attr_reader :quota_project_id
|
151
203
|
attr_accessor :universe_domain
|
@@ -169,16 +221,43 @@ module Google
|
|
169
221
|
@private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
|
170
222
|
self.class.read_json_key json_key_io
|
171
223
|
else
|
172
|
-
@private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
173
|
-
@issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
174
|
-
@project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
|
175
|
-
@quota_project_id =
|
176
|
-
@universe_domain =
|
224
|
+
@private_key = options.key?(:private_key) ? options[:private_key] : ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
225
|
+
@issuer = options.key?(:issuer) ? options[:issuer] : ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
226
|
+
@project_id = options.key?(:project_id) ? options[:project_id] : ENV[CredentialsLoader::PROJECT_ID_VAR]
|
227
|
+
@quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
|
228
|
+
@universe_domain = options[:universe_domain] if options.key? :universe_domain
|
177
229
|
end
|
178
230
|
@universe_domain ||= "googleapis.com"
|
179
231
|
@project_id ||= CredentialsLoader.load_gcloud_project_id
|
180
232
|
@signing_key = OpenSSL::PKey::RSA.new @private_key
|
181
|
-
@scope = options[:scope]
|
233
|
+
@scope = options[:scope] if options.key? :scope
|
234
|
+
@logger = options[:logger] if options.key? :scope
|
235
|
+
end
|
236
|
+
|
237
|
+
# Creates a duplicate of these credentials
|
238
|
+
#
|
239
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
240
|
+
# The following keys are recognized
|
241
|
+
# * `private key` the private key in string form
|
242
|
+
# * `issuer` the SA issuer
|
243
|
+
# * `scope` the scope(s) to access
|
244
|
+
# * `project_id` the project id to use during the authentication
|
245
|
+
# * `quota_project_id` the quota project id to use
|
246
|
+
# * `universe_domain` the universe domain of the credentials
|
247
|
+
def duplicate options = {}
|
248
|
+
options = deep_hash_normalize options
|
249
|
+
|
250
|
+
options = {
|
251
|
+
private_key: @private_key,
|
252
|
+
issuer: @issuer,
|
253
|
+
scope: @scope,
|
254
|
+
project_id: project_id,
|
255
|
+
quota_project_id: quota_project_id,
|
256
|
+
universe_domain: universe_domain,
|
257
|
+
logger: logger
|
258
|
+
}.merge(options)
|
259
|
+
|
260
|
+
self.class.new options
|
182
261
|
end
|
183
262
|
|
184
263
|
# Construct a jwt token if the JWT_AUD_URI key is present in the input
|
@@ -237,6 +316,23 @@ module Google
|
|
237
316
|
def needs_access_token?
|
238
317
|
false
|
239
318
|
end
|
319
|
+
|
320
|
+
private
|
321
|
+
|
322
|
+
def deep_hash_normalize old_hash
|
323
|
+
sym_hash = {}
|
324
|
+
old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
|
325
|
+
sym_hash
|
326
|
+
end
|
327
|
+
|
328
|
+
# Convert all keys in this hash (nested) to symbols for uniform retrieval
|
329
|
+
def recursive_hash_normalize_keys val
|
330
|
+
if val.is_a? Hash
|
331
|
+
deep_hash_normalize val
|
332
|
+
else
|
333
|
+
val
|
334
|
+
end
|
335
|
+
end
|
240
336
|
end
|
241
337
|
end
|
242
338
|
end
|
data/lib/googleauth/signet.rb
CHANGED
@@ -38,6 +38,22 @@ module Signet
|
|
38
38
|
self
|
39
39
|
end
|
40
40
|
|
41
|
+
alias update_signet_base update!
|
42
|
+
def update! options = {}
|
43
|
+
# Normalize all keys to symbols to allow indifferent access.
|
44
|
+
options = deep_hash_normalize options
|
45
|
+
|
46
|
+
# This `update!` method "overide" adds the `@logger`` update and
|
47
|
+
# the `universe_domain` update.
|
48
|
+
#
|
49
|
+
# The `universe_domain` is also updated in `update_token!` but is
|
50
|
+
# included here for completeness
|
51
|
+
self.universe_domain = options[:universe_domain] if options.key? :universe_domain
|
52
|
+
@logger = options[:logger] if options.key? :logger
|
53
|
+
|
54
|
+
update_signet_base options
|
55
|
+
end
|
56
|
+
|
41
57
|
def configure_connection options
|
42
58
|
@connection_info =
|
43
59
|
options[:connection_builder] || options[:default_connection]
|
@@ -117,6 +133,42 @@ module Signet
|
|
117
133
|
end
|
118
134
|
end
|
119
135
|
|
136
|
+
# Creates a duplicate of these credentials
|
137
|
+
# without the Signet::OAuth2::Client-specific
|
138
|
+
# transient state (e.g. cached tokens)
|
139
|
+
#
|
140
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
141
|
+
# @see Signet::OAuth2::Client#update!
|
142
|
+
def duplicate options = {}
|
143
|
+
options = deep_hash_normalize options
|
144
|
+
|
145
|
+
opts = {
|
146
|
+
authorization_uri: @authorization_uri,
|
147
|
+
token_credential_uri: @token_credential_uri,
|
148
|
+
client_id: @client_id,
|
149
|
+
client_secret: @client_secret,
|
150
|
+
scope: @scope,
|
151
|
+
target_audience: @target_audience,
|
152
|
+
redirect_uri: @redirect_uri,
|
153
|
+
username: @username,
|
154
|
+
password: @password,
|
155
|
+
issuer: @issuer,
|
156
|
+
person: @person,
|
157
|
+
sub: @sub,
|
158
|
+
audience: @audience,
|
159
|
+
signing_key: @signing_key,
|
160
|
+
extension_parameters: @extension_parameters,
|
161
|
+
additional_parameters: @additional_parameters,
|
162
|
+
access_type: @access_type,
|
163
|
+
universe_domain: @universe_domain,
|
164
|
+
logger: @logger
|
165
|
+
}.merge(options)
|
166
|
+
|
167
|
+
new_client = self.class.new opts
|
168
|
+
|
169
|
+
new_client.configure_connection options
|
170
|
+
end
|
171
|
+
|
120
172
|
private
|
121
173
|
|
122
174
|
def expires_at_from_id_token id_token
|
@@ -85,6 +85,26 @@ module Google
|
|
85
85
|
super options
|
86
86
|
end
|
87
87
|
|
88
|
+
# Creates a duplicate of these credentials
|
89
|
+
# without the Signet::OAuth2::Client-specific
|
90
|
+
# transient state (e.g. cached tokens)
|
91
|
+
#
|
92
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
93
|
+
# The following keys are recognized in addition to keys in the
|
94
|
+
# Signet::OAuth2::Client
|
95
|
+
# * `project_id` the project id to use during the authentication
|
96
|
+
# * `quota_project_id` the quota project id to use
|
97
|
+
# during the authentication
|
98
|
+
def duplicate options = {}
|
99
|
+
options = deep_hash_normalize options
|
100
|
+
super(
|
101
|
+
{
|
102
|
+
project_id: @project_id,
|
103
|
+
quota_project_id: @quota_project_id
|
104
|
+
}.merge(options)
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
88
108
|
# Revokes the credential
|
89
109
|
def revoke! options = {}
|
90
110
|
c = options[:connection] || Faraday.default_connection
|
@@ -114,6 +134,29 @@ module Google
|
|
114
134
|
Google::Auth::ScopeUtil.normalize(scope)
|
115
135
|
missing_scope.empty?
|
116
136
|
end
|
137
|
+
|
138
|
+
# Destructively updates these credentials
|
139
|
+
#
|
140
|
+
# This method is called by `Signet::OAuth2::Client`'s constructor
|
141
|
+
#
|
142
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
143
|
+
# The following keys are recognized in addition to keys in the
|
144
|
+
# Signet::OAuth2::Client
|
145
|
+
# * `project_id` the project id to use during the authentication
|
146
|
+
# * `quota_project_id` the quota project id to use
|
147
|
+
# during the authentication
|
148
|
+
# @return [Google::Auth::UserRefreshCredentials]
|
149
|
+
def update! options = {}
|
150
|
+
# Normalize all keys to symbols to allow indifferent access.
|
151
|
+
options = deep_hash_normalize options
|
152
|
+
|
153
|
+
@project_id = options[:project_id] if options.key? :project_id
|
154
|
+
@quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
|
155
|
+
|
156
|
+
super(options)
|
157
|
+
|
158
|
+
self
|
159
|
+
end
|
117
160
|
end
|
118
161
|
end
|
119
162
|
end
|
data/lib/googleauth/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: googleauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.13.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Emiola
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-01-24 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: faraday
|
@@ -166,6 +165,7 @@ files:
|
|
166
165
|
- lib/googleauth/id_tokens/errors.rb
|
167
166
|
- lib/googleauth/id_tokens/key_sources.rb
|
168
167
|
- lib/googleauth/id_tokens/verifier.rb
|
168
|
+
- lib/googleauth/impersonated_service_account.rb
|
169
169
|
- lib/googleauth/json_key_reader.rb
|
170
170
|
- lib/googleauth/oauth2/sts_client.rb
|
171
171
|
- lib/googleauth/scope_util.rb
|
@@ -185,7 +185,6 @@ metadata:
|
|
185
185
|
changelog_uri: https://github.com/googleapis/google-auth-library-ruby/blob/main/CHANGELOG.md
|
186
186
|
source_code_uri: https://github.com/googleapis/google-auth-library-ruby
|
187
187
|
bug_tracker_uri: https://github.com/googleapis/google-auth-library-ruby/issues
|
188
|
-
post_install_message:
|
189
188
|
rdoc_options: []
|
190
189
|
require_paths:
|
191
190
|
- lib
|
@@ -200,8 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
200
199
|
- !ruby/object:Gem::Version
|
201
200
|
version: '0'
|
202
201
|
requirements: []
|
203
|
-
rubygems_version: 3.
|
204
|
-
signing_key:
|
202
|
+
rubygems_version: 3.6.2
|
205
203
|
specification_version: 4
|
206
204
|
summary: Google Auth Library for Ruby
|
207
205
|
test_files: []
|