googleauth 1.12.0 → 1.13.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +9 -0
- data/lib/googleauth/base_client.rb +2 -0
- data/lib/googleauth/compute_engine.rb +52 -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,32 @@
|
|
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
|
+
|
18
|
+
### 1.12.2 (2024-12-19)
|
19
|
+
|
20
|
+
#### Bug Fixes
|
21
|
+
|
22
|
+
* GCECredentials lazily fetches from the metadata server to ensure a universe domain is known ([#509](https://github.com/googleapis/google-auth-library-ruby/issues/509))
|
23
|
+
|
24
|
+
### 1.12.1 (2024-12-17)
|
25
|
+
|
26
|
+
#### Bug Fixes
|
27
|
+
|
28
|
+
* Restored previous behavior where the apply! method returns the auth header ([#506](https://github.com/googleapis/google-auth-library-ruby/issues/506))
|
29
|
+
|
3
30
|
### 1.12.0 (2024-12-05)
|
4
31
|
|
5
32
|
#### Features
|
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,37 @@ 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
|
+
|
114
|
+
# @private
|
115
|
+
# Overrides universe_domain getter to fetch lazily if it hasn't been
|
116
|
+
# fetched yet. This is necessary specifically for Compute Engine because
|
117
|
+
# the universe comes from the metadata service, and isn't known
|
118
|
+
# immediately on credential construction. All other credential types read
|
119
|
+
# the universe from their json key or other immediate input.
|
120
|
+
def universe_domain
|
121
|
+
value = super
|
122
|
+
return value unless value.nil?
|
123
|
+
fetch_access_token!
|
124
|
+
super
|
125
|
+
end
|
126
|
+
|
96
127
|
# Overrides the super class method to change how access tokens are
|
97
128
|
# fetched.
|
98
129
|
def fetch_access_token _options = {}
|
@@ -123,6 +154,27 @@ module Google
|
|
123
154
|
end
|
124
155
|
end
|
125
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
|
+
|
126
178
|
private
|
127
179
|
|
128
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: []
|