vendor: run make vendor-update

This commit is contained in:
Aliaksandr Valialkin 2024-04-17 22:54:56 +02:00
parent bd454f5063
commit 184e4ef4d7
No known key found for this signature in database
GPG key ID: 52C003EE2BCDB9EB
52 changed files with 7222 additions and 10 deletions

6
go.mod
View file

@ -30,15 +30,16 @@ require (
github.com/valyala/gozstd v1.20.1
github.com/valyala/histogram v1.2.0
github.com/valyala/quicktemplate v1.7.0
golang.org/x/net v0.24.0 // indirect
golang.org/x/oauth2 v0.19.0
golang.org/x/sys v0.19.0
google.golang.org/api v0.173.0
google.golang.org/api v0.174.0
gopkg.in/yaml.v2 v2.4.0
)
require (
cloud.google.com/go v0.112.2 // indirect
cloud.google.com/go/auth v0.2.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.0 // indirect
cloud.google.com/go/compute/metadata v0.3.0 // indirect
cloud.google.com/go/iam v1.1.7 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 // indirect
@ -119,6 +120,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect

8
go.sum
View file

@ -15,6 +15,10 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw=
cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms=
cloud.google.com/go/auth v0.2.0 h1:y6oTcpMSbOcXbwYgUUrvI+mrQ2xbrcdpPgtVbCGTLTk=
cloud.google.com/go/auth v0.2.0/go.mod h1:+yb+oy3/P0geX6DLKlqiGHARGR6EX2GRtYCzWOCQSbU=
cloud.google.com/go/auth/oauth2adapt v0.2.0 h1:FR8zevgQwu+8CqiOT5r6xCmJa3pJC/wdXEEPF1OkNhA=
cloud.google.com/go/auth/oauth2adapt v0.2.0/go.mod h1:AfqujpDAlTfLfeCIl/HJZZlIxD8+nJoZ5e0x1IxGq5k=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -764,8 +768,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.173.0 h1:fz6B7GWYWLS/HfruiTsRYVKQQApJ6vasTYWAK6+Qo8g=
google.golang.org/api v0.173.0/go.mod h1:ins7pTzjeBPQ3SdC/plzki6d/dQWwAWy8qVZ4Vgkzl8=
google.golang.org/api v0.174.0 h1:zB1BWl7ocxfTea2aQ9mgdzXjnfPySllpPOskdnO+q34=
google.golang.org/api v0.174.0/go.mod h1:aC7tB6j0HR1Nl0ni5ghpx6iLasmAX78Zkh/wgxAAjLg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

79
vendor/cloud.google.com/go/auth/CHANGES.md generated vendored Normal file
View file

@ -0,0 +1,79 @@
# Changelog
## [0.2.0](https://github.com/googleapis/google-cloud-go/compare/auth/v0.1.1...auth/v0.2.0) (2024-04-15)
### Breaking Changes
In the below mentioned commits there were a few large breaking changes since the
last release of the module.
1. The `Credentials` type has been moved to the root of the module as it is
becoming the core abstraction for the whole module.
2. Because of the above mentioned change many functions that previously
returned a `TokenProvider` now return `Credentials`. Similarly, these
functions have been renamed to be more specific.
3. Most places that used to take an optional `TokenProvider` now accept
`Credentials`. You can make a `Credentials` from a `TokenProvider` using the
constructor found in the `auth` package.
4. The `detect` package has been renamed to `credentials`. With this change some
function signatures were also updated for better readability.
5. Derivative auth flows like `impersonate` and `downscope` have been moved to
be under the new `credentials` package.
Although these changes are disruptive we think that they are for the best of the
long-term health of the module. We do not expect any more large breaking changes
like these in future revisions, even before 1.0.0. This version will be the
first version of the auth library that our client libraries start to use and
depend on.
### Features
* **auth/credentials/externalaccount:** Add default TokenURL ([#9700](https://github.com/googleapis/google-cloud-go/issues/9700)) ([81830e6](https://github.com/googleapis/google-cloud-go/commit/81830e6848ceefd055aa4d08f933d1154455a0f6))
* **auth:** Add downscope.Options.UniverseDomain ([#9634](https://github.com/googleapis/google-cloud-go/issues/9634)) ([52cf7d7](https://github.com/googleapis/google-cloud-go/commit/52cf7d780853594291c4e34302d618299d1f5a1d))
* **auth:** Add universe domain to grpctransport and httptransport ([#9663](https://github.com/googleapis/google-cloud-go/issues/9663)) ([67d353b](https://github.com/googleapis/google-cloud-go/commit/67d353beefe3b607c08c891876fbd95ab89e5fe3)), refs [#9670](https://github.com/googleapis/google-cloud-go/issues/9670)
* **auth:** Add UniverseDomain to DetectOptions ([#9536](https://github.com/googleapis/google-cloud-go/issues/9536)) ([3618d3f](https://github.com/googleapis/google-cloud-go/commit/3618d3f7061615c0e189f376c75abc201203b501))
* **auth:** Make package externalaccount public ([#9633](https://github.com/googleapis/google-cloud-go/issues/9633)) ([a0978d8](https://github.com/googleapis/google-cloud-go/commit/a0978d8e96968399940ebd7d092539772bf9caac))
* **auth:** Move credentials to base auth package ([#9590](https://github.com/googleapis/google-cloud-go/issues/9590)) ([1a04baf](https://github.com/googleapis/google-cloud-go/commit/1a04bafa83c27342b9308d785645e1e5423ea10d))
* **auth:** Refactor public sigs to use Credentials ([#9603](https://github.com/googleapis/google-cloud-go/issues/9603)) ([69cb240](https://github.com/googleapis/google-cloud-go/commit/69cb240c530b1f7173a9af2555c19e9a1beb56c5))
### Bug Fixes
* **auth/oauth2adapt:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
* **auth:** Fix uint32 conversion ([9221c7f](https://github.com/googleapis/google-cloud-go/commit/9221c7fa12cef9d5fb7ddc92f41f1d6204971c7b))
* **auth:** Port sts expires fix ([#9618](https://github.com/googleapis/google-cloud-go/issues/9618)) ([7bec97b](https://github.com/googleapis/google-cloud-go/commit/7bec97b2f51ed3ac4f9b88bf100d301da3f5d1bd))
* **auth:** Read universe_domain from all credentials files ([#9632](https://github.com/googleapis/google-cloud-go/issues/9632)) ([16efbb5](https://github.com/googleapis/google-cloud-go/commit/16efbb52e39ea4a319e5ee1e95c0e0305b6d9824))
* **auth:** Remove content-type header from idms get requests ([#9508](https://github.com/googleapis/google-cloud-go/issues/9508)) ([8589f41](https://github.com/googleapis/google-cloud-go/commit/8589f41599d265d7c3d46a3d86c9fab2329cbdd9))
* **auth:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
## [0.1.1](https://github.com/googleapis/google-cloud-go/compare/auth/v0.1.0...auth/v0.1.1) (2024-03-10)
### Bug Fixes
* **auth/impersonate:** Properly send default detect params ([#9529](https://github.com/googleapis/google-cloud-go/issues/9529)) ([5b6b8be](https://github.com/googleapis/google-cloud-go/commit/5b6b8bef577f82707e51f5cc5d258d5bdf90218f)), refs [#9136](https://github.com/googleapis/google-cloud-go/issues/9136)
* **auth:** Update grpc-go to v1.56.3 ([343cea8](https://github.com/googleapis/google-cloud-go/commit/343cea8c43b1e31ae21ad50ad31d3b0b60143f8c))
* **auth:** Update grpc-go to v1.59.0 ([81a97b0](https://github.com/googleapis/google-cloud-go/commit/81a97b06cb28b25432e4ece595c55a9857e960b7))
## 0.1.0 (2023-10-18)
### Features
* **auth:** Add base auth package ([#8465](https://github.com/googleapis/google-cloud-go/issues/8465)) ([6a45f26](https://github.com/googleapis/google-cloud-go/commit/6a45f26b809b64edae21f312c18d4205f96b180e))
* **auth:** Add cert support to httptransport ([#8569](https://github.com/googleapis/google-cloud-go/issues/8569)) ([37e3435](https://github.com/googleapis/google-cloud-go/commit/37e3435f8e98595eafab481bdfcb31a4c56fa993))
* **auth:** Add Credentials.UniverseDomain() ([#8654](https://github.com/googleapis/google-cloud-go/issues/8654)) ([af0aa1e](https://github.com/googleapis/google-cloud-go/commit/af0aa1ed8015bc8fe0dd87a7549ae029107cbdb8))
* **auth:** Add detect package ([#8491](https://github.com/googleapis/google-cloud-go/issues/8491)) ([d977419](https://github.com/googleapis/google-cloud-go/commit/d977419a3269f6acc193df77a2136a6eb4b4add7))
* **auth:** Add downscope package ([#8532](https://github.com/googleapis/google-cloud-go/issues/8532)) ([dda9bff](https://github.com/googleapis/google-cloud-go/commit/dda9bff8ec70e6d104901b4105d13dcaa4e2404c))
* **auth:** Add grpctransport package ([#8625](https://github.com/googleapis/google-cloud-go/issues/8625)) ([69a8347](https://github.com/googleapis/google-cloud-go/commit/69a83470bdcc7ed10c6c36d1abc3b7cfdb8a0ee5))
* **auth:** Add httptransport package ([#8567](https://github.com/googleapis/google-cloud-go/issues/8567)) ([6898597](https://github.com/googleapis/google-cloud-go/commit/6898597d2ea95d630fcd00fd15c58c75ea843bff))
* **auth:** Add idtoken package ([#8580](https://github.com/googleapis/google-cloud-go/issues/8580)) ([a79e693](https://github.com/googleapis/google-cloud-go/commit/a79e693e97e4e3e1c6742099af3dbc58866d88fe))
* **auth:** Add impersonate package ([#8578](https://github.com/googleapis/google-cloud-go/issues/8578)) ([e29ba0c](https://github.com/googleapis/google-cloud-go/commit/e29ba0cb7bd3888ab9e808087027dc5a32474c04))
* **auth:** Add support for external accounts in detect ([#8508](https://github.com/googleapis/google-cloud-go/issues/8508)) ([62210d5](https://github.com/googleapis/google-cloud-go/commit/62210d5d3e56e8e9f35db8e6ac0defec19582507))
* **auth:** Port external account changes ([#8697](https://github.com/googleapis/google-cloud-go/issues/8697)) ([5823db5](https://github.com/googleapis/google-cloud-go/commit/5823db5d633069999b58b9131a7f9cd77e82c899))
### Bug Fixes
* **auth/oauth2adapt:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))
* **auth:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))

202
vendor/cloud.google.com/go/auth/LICENSE generated vendored Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

4
vendor/cloud.google.com/go/auth/README.md generated vendored Normal file
View file

@ -0,0 +1,4 @@
# auth
This module is currently EXPERIMENTAL and under active development. It is not
yet intended to be used.

476
vendor/cloud.google.com/go/auth/auth.go generated vendored Normal file
View file

@ -0,0 +1,476 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/jwt"
)
const (
// Parameter keys for AuthCodeURL method to support PKCE.
codeChallengeKey = "code_challenge"
codeChallengeMethodKey = "code_challenge_method"
// Parameter key for Exchange method to support PKCE.
codeVerifierKey = "code_verifier"
// 3 minutes and 45 seconds before expiration. The shortest MDS cache is 4 minutes,
// so we give it 15 seconds to refresh it's cache before attempting to refresh a token.
defaultExpiryDelta = 215 * time.Second
universeDomainDefault = "googleapis.com"
)
var (
defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
defaultHeader = &jwt.Header{Algorithm: jwt.HeaderAlgRSA256, Type: jwt.HeaderType}
// for testing
timeNow = time.Now
)
// TokenProvider specifies an interface for anything that can return a token.
type TokenProvider interface {
// Token returns a Token or an error.
// The Token returned must be safe to use
// concurrently.
// The returned Token must not be modified.
// The context provided must be sent along to any requests that are made in
// the implementing code.
Token(context.Context) (*Token, error)
}
// Token holds the credential token used to authorized requests. All fields are
// considered read-only.
type Token struct {
// Value is the token used to authorize requests. It is usually an access
// token but may be other types of tokens such as ID tokens in some flows.
Value string
// Type is the type of token Value is. If uninitialized, it should be
// assumed to be a "Bearer" token.
Type string
// Expiry is the time the token is set to expire.
Expiry time.Time
// Metadata may include, but is not limited to, the body of the token
// response returned by the server.
Metadata map[string]interface{} // TODO(codyoss): maybe make a method to flatten metadata to avoid []string for url.Values
}
// IsValid reports that a [Token] is non-nil, has a [Token.Value], and has not
// expired. A token is considered expired if [Token.Expiry] has passed or will
// pass in the next 10 seconds.
func (t *Token) IsValid() bool {
return t.isValidWithEarlyExpiry(defaultExpiryDelta)
}
func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
if t == nil || t.Value == "" {
return false
}
if t.Expiry.IsZero() {
return true
}
return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
}
// Credentials holds Google credentials, including
// [Application Default Credentials](https://developers.google.com/accounts/docs/application-default-credentials).
type Credentials struct {
json []byte
projectID CredentialsPropertyProvider
quotaProjectID CredentialsPropertyProvider
// universeDomain is the default service domain for a given Cloud universe.
universeDomain CredentialsPropertyProvider
TokenProvider
}
// JSON returns the bytes associated with the the file used to source
// credentials if one was used.
func (c *Credentials) JSON() []byte {
return c.json
}
// ProjectID returns the associated project ID from the underlying file or
// environment.
func (c *Credentials) ProjectID(ctx context.Context) (string, error) {
if c.projectID == nil {
return internal.GetProjectID(c.json, ""), nil
}
v, err := c.projectID.GetProperty(ctx)
if err != nil {
return "", err
}
return internal.GetProjectID(c.json, v), nil
}
// QuotaProjectID returns the associated quota project ID from the underlying
// file or environment.
func (c *Credentials) QuotaProjectID(ctx context.Context) (string, error) {
if c.quotaProjectID == nil {
return internal.GetQuotaProject(c.json, ""), nil
}
v, err := c.quotaProjectID.GetProperty(ctx)
if err != nil {
return "", err
}
return internal.GetQuotaProject(c.json, v), nil
}
// UniverseDomain returns the default service domain for a given Cloud universe.
// The default value is "googleapis.com".
func (c *Credentials) UniverseDomain(ctx context.Context) (string, error) {
if c.universeDomain == nil {
return universeDomainDefault, nil
}
v, err := c.universeDomain.GetProperty(ctx)
if err != nil {
return "", err
}
if v == "" {
return universeDomainDefault, nil
}
return v, err
}
// CredentialsPropertyProvider provides an implementation to fetch a property
// value for [Credentials].
type CredentialsPropertyProvider interface {
GetProperty(context.Context) (string, error)
}
// CredentialsPropertyFunc is a type adapter to allow the use of ordinary
// functions as a [CredentialsPropertyProvider].
type CredentialsPropertyFunc func(context.Context) (string, error)
// GetProperty loads the properly value provided the given context.
func (p CredentialsPropertyFunc) GetProperty(ctx context.Context) (string, error) {
return p(ctx)
}
// CredentialsOptions are used to configure [Credentials].
type CredentialsOptions struct {
// TokenProvider is a means of sourcing a token for the credentials. Required.
TokenProvider TokenProvider
// JSON is the raw contents of the credentials file if sourced from a file.
JSON []byte
// ProjectIDProvider resolves the project ID associated with the
// credentials.
ProjectIDProvider CredentialsPropertyProvider
// QuotaProjectIDProvider resolves the quota project ID associated with the
// credentials.
QuotaProjectIDProvider CredentialsPropertyProvider
// UniverseDomainProvider resolves the universe domain with the credentials.
UniverseDomainProvider CredentialsPropertyProvider
}
// NewCredentials returns new [Credentials] from the provided options. Most users
// will want to build this object a function from the
// [cloud.google.com/go/auth/credentials] package.
func NewCredentials(opts *CredentialsOptions) *Credentials {
creds := &Credentials{
TokenProvider: opts.TokenProvider,
json: opts.JSON,
projectID: opts.ProjectIDProvider,
quotaProjectID: opts.QuotaProjectIDProvider,
universeDomain: opts.UniverseDomainProvider,
}
return creds
}
// CachedTokenProviderOptions provided options for configuring a
// CachedTokenProvider.
type CachedTokenProviderOptions struct {
// DisableAutoRefresh makes the TokenProvider always return the same token,
// even if it is expired.
DisableAutoRefresh bool
// ExpireEarly configures the amount of time before a token expires, that it
// should be refreshed. If unset, the default value is 10 seconds.
ExpireEarly time.Duration
}
func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
if ctpo == nil {
return true
}
return !ctpo.DisableAutoRefresh
}
func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
if ctpo == nil {
return defaultExpiryDelta
}
return ctpo.ExpireEarly
}
// NewCachedTokenProvider wraps a [TokenProvider] to cache the tokens returned
// by the underlying provider. By default it will refresh tokens ten seconds
// before they expire, but this time can be configured with the optional
// options.
func NewCachedTokenProvider(tp TokenProvider, opts *CachedTokenProviderOptions) TokenProvider {
if ctp, ok := tp.(*cachedTokenProvider); ok {
return ctp
}
return &cachedTokenProvider{
tp: tp,
autoRefresh: opts.autoRefresh(),
expireEarly: opts.expireEarly(),
}
}
type cachedTokenProvider struct {
tp TokenProvider
autoRefresh bool
expireEarly time.Duration
mu sync.Mutex
cachedToken *Token
}
func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.cachedToken.IsValid() || !c.autoRefresh {
return c.cachedToken, nil
}
t, err := c.tp.Token(ctx)
if err != nil {
return nil, err
}
c.cachedToken = t
return t, nil
}
// Error is a error associated with retrieving a [Token]. It can hold useful
// additional details for debugging.
type Error struct {
// Response is the HTTP response associated with error. The body will always
// be already closed and consumed.
Response *http.Response
// Body is the HTTP response body.
Body []byte
// Err is the underlying wrapped error.
Err error
// code returned in the token response
code string
// description returned in the token response
description string
// uri returned in the token response
uri string
}
func (e *Error) Error() string {
if e.code != "" {
s := fmt.Sprintf("auth: %q", e.code)
if e.description != "" {
s += fmt.Sprintf(" %q", e.description)
}
if e.uri != "" {
s += fmt.Sprintf(" %q", e.uri)
}
return s
}
return fmt.Sprintf("auth: cannot fetch token: %v\nResponse: %s", e.Response.StatusCode, e.Body)
}
// Temporary returns true if the error is considered temporary and may be able
// to be retried.
func (e *Error) Temporary() bool {
if e.Response == nil {
return false
}
sc := e.Response.StatusCode
return sc == http.StatusInternalServerError || sc == http.StatusServiceUnavailable || sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests
}
func (e *Error) Unwrap() error {
return e.Err
}
// Style describes how the token endpoint wants to receive the ClientID and
// ClientSecret.
type Style int
const (
// StyleUnknown means the value has not been initiated. Sending this in
// a request will cause the token exchange to fail.
StyleUnknown Style = iota
// StyleInParams sends client info in the body of a POST request.
StyleInParams
// StyleInHeader sends client info using Basic Authorization header.
StyleInHeader
)
// Options2LO is the configuration settings for doing a 2-legged JWT OAuth2 flow.
type Options2LO struct {
// Email is the OAuth2 client ID. This value is set as the "iss" in the
// JWT.
Email string
// PrivateKey contains the contents of an RSA private key or the
// contents of a PEM file that contains a private key. It is used to sign
// the JWT created.
PrivateKey []byte
// TokenURL is th URL the JWT is sent to. Required.
TokenURL string
// PrivateKeyID is the ID of the key used to sign the JWT. It is used as the
// "kid" in the JWT header. Optional.
PrivateKeyID string
// Subject is the used for to impersonate a user. It is used as the "sub" in
// the JWT.m Optional.
Subject string
// Scopes specifies requested permissions for the token. Optional.
Scopes []string
// Expires specifies the lifetime of the token. Optional.
Expires time.Duration
// Audience specifies the "aud" in the JWT. Optional.
Audience string
// PrivateClaims allows specifying any custom claims for the JWT. Optional.
PrivateClaims map[string]interface{}
// Client is the client to be used to make the underlying token requests.
// Optional.
Client *http.Client
// UseIDToken requests that the token returned be an ID token if one is
// returned from the server. Optional.
UseIDToken bool
}
func (o *Options2LO) client() *http.Client {
if o.Client != nil {
return o.Client
}
return internal.CloneDefaultClient()
}
func (o *Options2LO) validate() error {
if o == nil {
return errors.New("auth: options must be provided")
}
if o.Email == "" {
return errors.New("auth: email must be provided")
}
if len(o.PrivateKey) == 0 {
return errors.New("auth: private key must be provided")
}
if o.TokenURL == "" {
return errors.New("auth: token URL must be provided")
}
return nil
}
// New2LOTokenProvider returns a [TokenProvider] from the provided options.
func New2LOTokenProvider(opts *Options2LO) (TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
return tokenProvider2LO{opts: opts, Client: opts.client()}, nil
}
type tokenProvider2LO struct {
opts *Options2LO
Client *http.Client
}
func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) {
pk, err := internal.ParseKey(tp.opts.PrivateKey)
if err != nil {
return nil, err
}
claimSet := &jwt.Claims{
Iss: tp.opts.Email,
Scope: strings.Join(tp.opts.Scopes, " "),
Aud: tp.opts.TokenURL,
AdditionalClaims: tp.opts.PrivateClaims,
Sub: tp.opts.Subject,
}
if t := tp.opts.Expires; t > 0 {
claimSet.Exp = time.Now().Add(t).Unix()
}
if aud := tp.opts.Audience; aud != "" {
claimSet.Aud = aud
}
h := *defaultHeader
h.KeyID = tp.opts.PrivateKeyID
payload, err := jwt.EncodeJWS(&h, claimSet, pk)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("grant_type", defaultGrantType)
v.Set("assertion", payload)
resp, err := tp.Client.PostForm(tp.opts.TokenURL, v)
if err != nil {
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
}
defer resp.Body.Close()
body, err := internal.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
}
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
return nil, &Error{
Response: resp,
Body: body,
}
}
// tokenRes is the JSON response body.
var tokenRes struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
IDToken string `json:"id_token"`
ExpiresIn int64 `json:"expires_in"`
}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
}
token := &Token{
Value: tokenRes.AccessToken,
Type: tokenRes.TokenType,
}
token.Metadata = make(map[string]interface{})
json.Unmarshal(body, &token.Metadata) // no error checks for optional fields
if secs := tokenRes.ExpiresIn; secs > 0 {
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
}
if v := tokenRes.IDToken; v != "" {
// decode returned id token to get expiry
claimSet, err := jwt.DecodeJWS(v)
if err != nil {
return nil, fmt.Errorf("auth: error decoding JWT token: %w", err)
}
token.Expiry = time.Unix(claimSet.Exp, 0)
}
if tp.opts.UseIDToken {
if tokenRes.IDToken == "" {
return nil, fmt.Errorf("auth: response doesn't have JWT token")
}
token.Value = tokenRes.IDToken
}
return token, nil
}

85
vendor/cloud.google.com/go/auth/credentials/compute.go generated vendored Normal file
View file

@ -0,0 +1,85 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credentials
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/compute/metadata"
)
var (
computeTokenMetadata = map[string]interface{}{
"auth.google.tokenSource": "compute-metadata",
"auth.google.serviceAccount": "default",
}
computeTokenURI = "instance/service-accounts/default/token"
)
// computeTokenProvider creates a [cloud.google.com/go/auth.TokenProvider] that
// uses the metadata service to retrieve tokens.
func computeTokenProvider(earlyExpiry time.Duration, scope ...string) auth.TokenProvider {
return auth.NewCachedTokenProvider(computeProvider{scopes: scope}, &auth.CachedTokenProviderOptions{
ExpireEarly: earlyExpiry,
})
}
// computeProvider fetches tokens from the google cloud metadata service.
type computeProvider struct {
scopes []string
}
type metadataTokenResp struct {
AccessToken string `json:"access_token"`
ExpiresInSec int `json:"expires_in"`
TokenType string `json:"token_type"`
}
func (cs computeProvider) Token(ctx context.Context) (*auth.Token, error) {
tokenURI, err := url.Parse(computeTokenURI)
if err != nil {
return nil, err
}
if len(cs.scopes) > 0 {
v := url.Values{}
v.Set("scopes", strings.Join(cs.scopes, ","))
tokenURI.RawQuery = v.Encode()
}
tokenJSON, err := metadata.Get(tokenURI.String())
if err != nil {
return nil, err
}
var res metadataTokenResp
if err := json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res); err != nil {
return nil, fmt.Errorf("credentials: invalid token JSON from metadata: %w", err)
}
if res.ExpiresInSec == 0 || res.AccessToken == "" {
return nil, errors.New("credentials: incomplete token received from metadata")
}
return &auth.Token{
Value: res.AccessToken,
Type: res.TokenType,
Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
Metadata: computeTokenMetadata,
}, nil
}

249
vendor/cloud.google.com/go/auth/credentials/detect.go generated vendored Normal file
View file

@ -0,0 +1,249 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credentials
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/compute/metadata"
)
const (
// jwtTokenURL is Google's OAuth 2.0 token URL to use with the JWT(2LO) flow.
jwtTokenURL = "https://oauth2.googleapis.com/token"
// Google's OAuth 2.0 default endpoints.
googleAuthURL = "https://accounts.google.com/o/oauth2/auth"
googleTokenURL = "https://oauth2.googleapis.com/token"
// Help on default credentials
adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
)
var (
// for testing
allowOnGCECheck = true
)
// OnGCE reports whether this process is running in Google Cloud.
func OnGCE() bool {
// TODO(codyoss): once all libs use this auth lib move metadata check here
return allowOnGCECheck && metadata.OnGCE()
}
// DetectDefault searches for "Application Default Credentials" and returns
// a credential based on the [DetectOptions] provided.
//
// It looks for credentials in the following places, preferring the first
// location found:
//
// - A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS
// environment variable. For workload identity federation, refer to
// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation
// on how to generate the JSON configuration file for on-prem/non-Google
// cloud platforms.
// - A JSON file in a location known to the gcloud command-line tool. On
// Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On
// other systems, $HOME/.config/gcloud/application_default_credentials.json.
// - On Google Compute Engine, Google App Engine standard second generation
// runtimes, and Google App Engine flexible environment, it fetches
// credentials from the metadata server.
func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) {
if err := opts.validate(); err != nil {
return nil, err
}
if opts.CredentialsJSON != nil {
return readCredentialsFileJSON(opts.CredentialsJSON, opts)
}
if filename := credsfile.GetFileNameFromEnv(opts.CredentialsFile); filename != "" {
if creds, err := readCredentialsFile(filename, opts); err == nil {
return creds, err
}
}
fileName := credsfile.GetWellKnownFileName()
if b, err := os.ReadFile(fileName); err == nil {
return readCredentialsFileJSON(b, opts)
}
if OnGCE() {
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: computeTokenProvider(opts.EarlyTokenRefresh, opts.Scopes...),
ProjectIDProvider: auth.CredentialsPropertyFunc(func(context.Context) (string, error) {
return metadata.ProjectID()
}),
UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{},
}), nil
}
return nil, fmt.Errorf("credentials: could not find default credentials. See %v for more information", adcSetupURL)
}
// DetectOptions provides configuration for [DetectDefault].
type DetectOptions struct {
// Scopes that credentials tokens should have. Example:
// https://www.googleapis.com/auth/cloud-platform. Required if Audience is
// not provided.
Scopes []string
// Audience that credentials tokens should have. Only applicable for 2LO
// flows with service accounts. If specified, scopes should not be provided.
Audience string
// Subject is the user email used for [domain wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority).
// Optional.
Subject string
// EarlyTokenRefresh configures how early before a token expires that it
// should be refreshed.
EarlyTokenRefresh time.Duration
// AuthHandlerOptions configures an authorization handler and other options
// for 3LO flows. It is required, and only used, for client credential
// flows.
AuthHandlerOptions *auth.AuthorizationHandlerOptions
// TokenURL allows to set the token endpoint for user credential flows. If
// unset the default value is: https://oauth2.googleapis.com/token.
// Optional.
TokenURL string
// STSAudience is the audience sent to when retrieving an STS token.
// Currently this only used for GDCH auth flow, for which it is required.
STSAudience string
// CredentialsFile overrides detection logic and sources a credential file
// from the provided filepath. If provided, CredentialsJSON must not be.
// Optional.
CredentialsFile string
// CredentialsJSON overrides detection logic and uses the JSON bytes as the
// source for the credential. If provided, CredentialsFile must not be.
// Optional.
CredentialsJSON []byte
// UseSelfSignedJWT directs service account based credentials to create a
// self-signed JWT with the private key found in the file, skipping any
// network requests that would normally be made. Optional.
UseSelfSignedJWT bool
// Client configures the underlying client used to make network requests
// when fetching tokens. Optional.
Client *http.Client
// UniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com". This option is ignored for
// authentication flows that do not support universe domain. Optional.
UniverseDomain string
}
func (o *DetectOptions) validate() error {
if o == nil {
return errors.New("credentials: options must be provided")
}
if len(o.Scopes) > 0 && o.Audience != "" {
return errors.New("credentials: both scopes and audience were provided")
}
if len(o.CredentialsJSON) > 0 && o.CredentialsFile != "" {
return errors.New("credentials: both credentials file and JSON were provided")
}
return nil
}
func (o *DetectOptions) tokenURL() string {
if o.TokenURL != "" {
return o.TokenURL
}
return googleTokenURL
}
func (o *DetectOptions) scopes() []string {
scopes := make([]string, len(o.Scopes))
copy(scopes, o.Scopes)
return scopes
}
func (o *DetectOptions) client() *http.Client {
if o.Client != nil {
return o.Client
}
return internal.CloneDefaultClient()
}
func readCredentialsFile(filename string, opts *DetectOptions) (*auth.Credentials, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return readCredentialsFileJSON(b, opts)
}
func readCredentialsFileJSON(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
// attempt to parse jsonData as a Google Developers Console client_credentials.json.
config := clientCredConfigFromJSON(b, opts)
if config != nil {
if config.AuthHandlerOpts == nil {
return nil, errors.New("credentials: auth handler must be specified for this credential filetype")
}
tp, err := auth.New3LOTokenProvider(config)
if err != nil {
return nil, err
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: tp,
JSON: b,
}), nil
}
return fileCredentials(b, opts)
}
func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO {
var creds credsfile.ClientCredentialsFile
var c *credsfile.Config3LO
if err := json.Unmarshal(b, &creds); err != nil {
return nil
}
switch {
case creds.Web != nil:
c = creds.Web
case creds.Installed != nil:
c = creds.Installed
default:
return nil
}
if len(c.RedirectURIs) < 1 {
return nil
}
var handleOpts *auth.AuthorizationHandlerOptions
if opts.AuthHandlerOptions != nil {
handleOpts = &auth.AuthorizationHandlerOptions{
Handler: opts.AuthHandlerOptions.Handler,
State: opts.AuthHandlerOptions.State,
PKCEOpts: opts.AuthHandlerOptions.PKCEOpts,
}
}
return &auth.Options3LO{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
RedirectURL: c.RedirectURIs[0],
Scopes: opts.scopes(),
AuthURL: c.AuthURI,
TokenURL: c.TokenURI,
Client: opts.client(),
EarlyTokenExpiry: opts.EarlyTokenRefresh,
AuthHandlerOpts: handleOpts,
// TODO(codyoss): refactor this out. We need to add in auto-detection
// for this use case.
AuthStyle: auth.StyleInParams,
}
}

45
vendor/cloud.google.com/go/auth/credentials/doc.go generated vendored Normal file
View file

@ -0,0 +1,45 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package credentials provides support for making OAuth2 authorized and
// authenticated HTTP requests to Google APIs. It supports the Web server flow,
// client-side credentials, service accounts, Google Compute Engine service
// accounts, Google App Engine service accounts and workload identity federation
// from non-Google cloud platforms.
//
// A brief overview of the package follows. For more information, please read
// https://developers.google.com/accounts/docs/OAuth2
// and
// https://developers.google.com/accounts/docs/application-default-credentials.
// For more information on using workload identity federation, refer to
// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation.
//
// # Credentials
//
// The [cloud.google.com/go/auth.Credentials] type represents Google
// credentials, including Application Default Credentials.
//
// Use [DetectDefault] to obtain Application Default Credentials.
//
// Application Default Credentials support workload identity federation to
// access Google Cloud resources from non-Google Cloud platforms including Amazon
// Web Services (AWS), Microsoft Azure or any identity provider that supports
// OpenID Connect (OIDC). Workload identity federation is recommended for
// non-Google Cloud environments as it avoids the need to download, manage, and
// store service account private keys locally.
//
// # Workforce Identity Federation
//
// For more information on this feature see [cloud.google.com/go/auth/credentials/externalaccount].
package credentials

View file

@ -0,0 +1,219 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credentials
import (
"errors"
"fmt"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/internal/externalaccount"
"cloud.google.com/go/auth/credentials/internal/externalaccountuser"
"cloud.google.com/go/auth/credentials/internal/gdch"
"cloud.google.com/go/auth/credentials/internal/impersonate"
internalauth "cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
)
func fileCredentials(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
fileType, err := credsfile.ParseFileType(b)
if err != nil {
return nil, err
}
var projectID, quotaProjectID, universeDomain string
var tp auth.TokenProvider
switch fileType {
case credsfile.ServiceAccountKey:
f, err := credsfile.ParseServiceAccount(b)
if err != nil {
return nil, err
}
tp, err = handleServiceAccount(f, opts)
if err != nil {
return nil, err
}
projectID = f.ProjectID
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
case credsfile.UserCredentialsKey:
f, err := credsfile.ParseUserCredentials(b)
if err != nil {
return nil, err
}
tp, err = handleUserCredential(f, opts)
if err != nil {
return nil, err
}
quotaProjectID = f.QuotaProjectID
universeDomain = f.UniverseDomain
case credsfile.ExternalAccountKey:
f, err := credsfile.ParseExternalAccount(b)
if err != nil {
return nil, err
}
tp, err = handleExternalAccount(f, opts)
if err != nil {
return nil, err
}
quotaProjectID = f.QuotaProjectID
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
case credsfile.ExternalAccountAuthorizedUserKey:
f, err := credsfile.ParseExternalAccountAuthorizedUser(b)
if err != nil {
return nil, err
}
tp, err = handleExternalAccountAuthorizedUser(f, opts)
if err != nil {
return nil, err
}
quotaProjectID = f.QuotaProjectID
universeDomain = f.UniverseDomain
case credsfile.ImpersonatedServiceAccountKey:
f, err := credsfile.ParseImpersonatedServiceAccount(b)
if err != nil {
return nil, err
}
tp, err = handleImpersonatedServiceAccount(f, opts)
if err != nil {
return nil, err
}
universeDomain = resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain)
case credsfile.GDCHServiceAccountKey:
f, err := credsfile.ParseGDCHServiceAccount(b)
if err != nil {
return nil, err
}
tp, err = handleGDCHServiceAccount(f, opts)
if err != nil {
return nil, err
}
projectID = f.Project
universeDomain = f.UniverseDomain
default:
return nil, fmt.Errorf("credentials: unsupported filetype %q", fileType)
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: auth.NewCachedTokenProvider(tp, &auth.CachedTokenProviderOptions{
ExpireEarly: opts.EarlyTokenRefresh,
}),
JSON: b,
ProjectIDProvider: internalauth.StaticCredentialsProperty(projectID),
QuotaProjectIDProvider: internalauth.StaticCredentialsProperty(quotaProjectID),
UniverseDomainProvider: internalauth.StaticCredentialsProperty(universeDomain),
}), nil
}
// resolveUniverseDomain returns optsUniverseDomain if non-empty, in order to
// support configuring universe-specific credentials in code. Auth flows
// unsupported for universe domain should not use this func, but should instead
// simply set the file universe domain on the credentials.
func resolveUniverseDomain(optsUniverseDomain, fileUniverseDomain string) string {
if optsUniverseDomain != "" {
return optsUniverseDomain
}
return fileUniverseDomain
}
func handleServiceAccount(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
if opts.UseSelfSignedJWT {
return configureSelfSignedJWT(f, opts)
}
opts2LO := &auth.Options2LO{
Email: f.ClientEmail,
PrivateKey: []byte(f.PrivateKey),
PrivateKeyID: f.PrivateKeyID,
Scopes: opts.scopes(),
TokenURL: f.TokenURL,
Subject: opts.Subject,
}
if opts2LO.TokenURL == "" {
opts2LO.TokenURL = jwtTokenURL
}
return auth.New2LOTokenProvider(opts2LO)
}
func handleUserCredential(f *credsfile.UserCredentialsFile, opts *DetectOptions) (auth.TokenProvider, error) {
opts3LO := &auth.Options3LO{
ClientID: f.ClientID,
ClientSecret: f.ClientSecret,
Scopes: opts.scopes(),
AuthURL: googleAuthURL,
TokenURL: opts.tokenURL(),
AuthStyle: auth.StyleInParams,
EarlyTokenExpiry: opts.EarlyTokenRefresh,
RefreshToken: f.RefreshToken,
}
return auth.New3LOTokenProvider(opts3LO)
}
func handleExternalAccount(f *credsfile.ExternalAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
externalOpts := &externalaccount.Options{
Audience: f.Audience,
SubjectTokenType: f.SubjectTokenType,
TokenURL: f.TokenURL,
TokenInfoURL: f.TokenInfoURL,
ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
ClientSecret: f.ClientSecret,
ClientID: f.ClientID,
CredentialSource: f.CredentialSource,
QuotaProjectID: f.QuotaProjectID,
Scopes: opts.scopes(),
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
Client: opts.client(),
}
if f.ServiceAccountImpersonation != nil {
externalOpts.ServiceAccountImpersonationLifetimeSeconds = f.ServiceAccountImpersonation.TokenLifetimeSeconds
}
return externalaccount.NewTokenProvider(externalOpts)
}
func handleExternalAccountAuthorizedUser(f *credsfile.ExternalAccountAuthorizedUserFile, opts *DetectOptions) (auth.TokenProvider, error) {
externalOpts := &externalaccountuser.Options{
Audience: f.Audience,
RefreshToken: f.RefreshToken,
TokenURL: f.TokenURL,
TokenInfoURL: f.TokenInfoURL,
ClientID: f.ClientID,
ClientSecret: f.ClientSecret,
Scopes: opts.scopes(),
Client: opts.client(),
}
return externalaccountuser.NewTokenProvider(externalOpts)
}
func handleImpersonatedServiceAccount(f *credsfile.ImpersonatedServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
if f.ServiceAccountImpersonationURL == "" || f.CredSource == nil {
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
}
tp, err := fileCredentials(f.CredSource, opts)
if err != nil {
return nil, err
}
return impersonate.NewTokenProvider(&impersonate.Options{
URL: f.ServiceAccountImpersonationURL,
Scopes: opts.scopes(),
Tp: tp,
Delegates: f.Delegates,
Client: opts.client(),
})
}
func handleGDCHServiceAccount(f *credsfile.GDCHServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
return gdch.NewTokenProvider(f, &gdch.Options{
STSAudience: opts.STSAudience,
Client: opts.client(),
})
}

View file

@ -0,0 +1,547 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path"
"sort"
"strings"
"time"
"cloud.google.com/go/auth/internal"
)
var (
// getenv aliases os.Getenv for testing
getenv = os.Getenv
)
const (
// AWS Signature Version 4 signing algorithm identifier.
awsAlgorithm = "AWS4-HMAC-SHA256"
// The termination string for the AWS credential scope value as defined in
// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
awsRequestType = "aws4_request"
// The AWS authorization header name for the security session token if available.
awsSecurityTokenHeader = "x-amz-security-token"
// The name of the header containing the session token for metadata endpoint calls
awsIMDSv2SessionTokenHeader = "X-aws-ec2-metadata-token"
awsIMDSv2SessionTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
awsIMDSv2SessionTTL = "300"
// The AWS authorization header name for the auto-generated date.
awsDateHeader = "x-amz-date"
defaultRegionalCredentialVerificationURL = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
// Supported AWS configuration environment variables.
awsAccessKeyIDEnvVar = "AWS_ACCESS_KEY_ID"
awsDefaultRegionEnvVar = "AWS_DEFAULT_REGION"
awsRegionEnvVar = "AWS_REGION"
awsSecretAccessKeyEnvVar = "AWS_SECRET_ACCESS_KEY"
awsSessionTokenEnvVar = "AWS_SESSION_TOKEN"
awsTimeFormatLong = "20060102T150405Z"
awsTimeFormatShort = "20060102"
awsProviderType = "aws"
)
type awsSubjectProvider struct {
EnvironmentID string
RegionURL string
RegionalCredVerificationURL string
CredVerificationURL string
IMDSv2SessionTokenURL string
TargetResource string
requestSigner *awsRequestSigner
region string
securityCredentialsProvider AwsSecurityCredentialsProvider
reqOpts *RequestOptions
Client *http.Client
}
func (sp *awsSubjectProvider) subjectToken(ctx context.Context) (string, error) {
// Set Defaults
if sp.RegionalCredVerificationURL == "" {
sp.RegionalCredVerificationURL = defaultRegionalCredentialVerificationURL
}
if sp.requestSigner == nil {
headers := make(map[string]string)
if sp.shouldUseMetadataServer() {
awsSessionToken, err := sp.getAWSSessionToken(ctx)
if err != nil {
return "", err
}
if awsSessionToken != "" {
headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
}
}
awsSecurityCredentials, err := sp.getSecurityCredentials(ctx, headers)
if err != nil {
return "", err
}
if sp.region, err = sp.getRegion(ctx, headers); err != nil {
return "", err
}
sp.requestSigner = &awsRequestSigner{
RegionName: sp.region,
AwsSecurityCredentials: awsSecurityCredentials,
}
}
// Generate the signed request to AWS STS GetCallerIdentity API.
// Use the required regional endpoint. Otherwise, the request will fail.
req, err := http.NewRequest("POST", strings.Replace(sp.RegionalCredVerificationURL, "{region}", sp.region, 1), nil)
if err != nil {
return "", err
}
// The full, canonical resource name of the workload identity pool
// provider, with or without the HTTPS prefix.
// Including this header as part of the signature is recommended to
// ensure data integrity.
if sp.TargetResource != "" {
req.Header.Set("x-goog-cloud-target-resource", sp.TargetResource)
}
sp.requestSigner.signRequest(req)
/*
The GCP STS endpoint expects the headers to be formatted as:
# [
# {key: 'x-amz-date', value: '...'},
# {key: 'Authorization', value: '...'},
# ...
# ]
# And then serialized as:
# quote(json.dumps({
# url: '...',
# method: 'POST',
# headers: [{key: 'x-amz-date', value: '...'}, ...]
# }))
*/
awsSignedReq := awsRequest{
URL: req.URL.String(),
Method: "POST",
}
for headerKey, headerList := range req.Header {
for _, headerValue := range headerList {
awsSignedReq.Headers = append(awsSignedReq.Headers, awsRequestHeader{
Key: headerKey,
Value: headerValue,
})
}
}
sort.Slice(awsSignedReq.Headers, func(i, j int) bool {
headerCompare := strings.Compare(awsSignedReq.Headers[i].Key, awsSignedReq.Headers[j].Key)
if headerCompare == 0 {
return strings.Compare(awsSignedReq.Headers[i].Value, awsSignedReq.Headers[j].Value) < 0
}
return headerCompare < 0
})
result, err := json.Marshal(awsSignedReq)
if err != nil {
return "", err
}
return url.QueryEscape(string(result)), nil
}
func (sp *awsSubjectProvider) providerType() string {
if sp.securityCredentialsProvider != nil {
return programmaticProviderType
}
return awsProviderType
}
func (sp *awsSubjectProvider) getAWSSessionToken(ctx context.Context) (string, error) {
if sp.IMDSv2SessionTokenURL == "" {
return "", nil
}
req, err := http.NewRequestWithContext(ctx, "PUT", sp.IMDSv2SessionTokenURL, nil)
if err != nil {
return "", err
}
req.Header.Set(awsIMDSv2SessionTTLHeader, awsIMDSv2SessionTTL)
resp, err := sp.Client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
respBody, err := internal.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("credentials: unable to retrieve AWS session token: %s", respBody)
}
return string(respBody), nil
}
func (sp *awsSubjectProvider) getRegion(ctx context.Context, headers map[string]string) (string, error) {
if sp.securityCredentialsProvider != nil {
return sp.securityCredentialsProvider.AwsRegion(ctx, sp.reqOpts)
}
if canRetrieveRegionFromEnvironment() {
if envAwsRegion := getenv(awsRegionEnvVar); envAwsRegion != "" {
return envAwsRegion, nil
}
return getenv(awsDefaultRegionEnvVar), nil
}
if sp.RegionURL == "" {
return "", errors.New("credentials: unable to determine AWS region")
}
req, err := http.NewRequestWithContext(ctx, "GET", sp.RegionURL, nil)
if err != nil {
return "", err
}
for name, value := range headers {
req.Header.Add(name, value)
}
resp, err := sp.Client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
respBody, err := internal.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("credentials: unable to retrieve AWS region - %s", respBody)
}
// This endpoint will return the region in format: us-east-2b.
// Only the us-east-2 part should be used.
bodyLen := len(respBody)
if bodyLen == 0 {
return "", nil
}
return string(respBody[:bodyLen-1]), nil
}
func (sp *awsSubjectProvider) getSecurityCredentials(ctx context.Context, headers map[string]string) (result *AwsSecurityCredentials, err error) {
if sp.securityCredentialsProvider != nil {
return sp.securityCredentialsProvider.AwsSecurityCredentials(ctx, sp.reqOpts)
}
if canRetrieveSecurityCredentialFromEnvironment() {
return &AwsSecurityCredentials{
AccessKeyID: getenv(awsAccessKeyIDEnvVar),
SecretAccessKey: getenv(awsSecretAccessKeyEnvVar),
SessionToken: getenv(awsSessionTokenEnvVar),
}, nil
}
roleName, err := sp.getMetadataRoleName(ctx, headers)
if err != nil {
return
}
credentials, err := sp.getMetadataSecurityCredentials(ctx, roleName, headers)
if err != nil {
return
}
if credentials.AccessKeyID == "" {
return result, errors.New("credentials: missing AccessKeyId credential")
}
if credentials.SecretAccessKey == "" {
return result, errors.New("credentials: missing SecretAccessKey credential")
}
return credentials, nil
}
func (sp *awsSubjectProvider) getMetadataSecurityCredentials(ctx context.Context, roleName string, headers map[string]string) (*AwsSecurityCredentials, error) {
var result *AwsSecurityCredentials
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s", sp.CredVerificationURL, roleName), nil)
if err != nil {
return result, err
}
for name, value := range headers {
req.Header.Add(name, value)
}
resp, err := sp.Client.Do(req)
if err != nil {
return result, err
}
defer resp.Body.Close()
respBody, err := internal.ReadAll(resp.Body)
if err != nil {
return result, err
}
if resp.StatusCode != http.StatusOK {
return result, fmt.Errorf("credentials: unable to retrieve AWS security credentials - %s", respBody)
}
err = json.Unmarshal(respBody, &result)
return result, err
}
func (sp *awsSubjectProvider) getMetadataRoleName(ctx context.Context, headers map[string]string) (string, error) {
if sp.CredVerificationURL == "" {
return "", errors.New("credentials: unable to determine the AWS metadata server security credentials endpoint")
}
req, err := http.NewRequestWithContext(ctx, "GET", sp.CredVerificationURL, nil)
if err != nil {
return "", err
}
for name, value := range headers {
req.Header.Add(name, value)
}
resp, err := sp.Client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
respBody, err := internal.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("credentials: unable to retrieve AWS role name - %s", respBody)
}
return string(respBody), nil
}
// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature.
type awsRequestSigner struct {
RegionName string
AwsSecurityCredentials *AwsSecurityCredentials
}
// signRequest adds the appropriate headers to an http.Request
// or returns an error if something prevented this.
func (rs *awsRequestSigner) signRequest(req *http.Request) error {
// req is assumed non-nil
signedRequest := cloneRequest(req)
timestamp := Now()
signedRequest.Header.Set("host", requestHost(req))
if rs.AwsSecurityCredentials.SessionToken != "" {
signedRequest.Header.Set(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SessionToken)
}
if signedRequest.Header.Get("date") == "" {
signedRequest.Header.Set(awsDateHeader, timestamp.Format(awsTimeFormatLong))
}
authorizationCode, err := rs.generateAuthentication(signedRequest, timestamp)
if err != nil {
return err
}
signedRequest.Header.Set("Authorization", authorizationCode)
req.Header = signedRequest.Header
return nil
}
func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp time.Time) (string, error) {
canonicalHeaderColumns, canonicalHeaderData := canonicalHeaders(req)
dateStamp := timestamp.Format(awsTimeFormatShort)
serviceName := ""
if splitHost := strings.Split(requestHost(req), "."); len(splitHost) > 0 {
serviceName = splitHost[0]
}
credentialScope := strings.Join([]string{dateStamp, rs.RegionName, serviceName, awsRequestType}, "/")
requestString, err := canonicalRequest(req, canonicalHeaderColumns, canonicalHeaderData)
if err != nil {
return "", err
}
requestHash, err := getSha256([]byte(requestString))
if err != nil {
return "", err
}
stringToSign := strings.Join([]string{awsAlgorithm, timestamp.Format(awsTimeFormatLong), credentialScope, requestHash}, "\n")
signingKey := []byte("AWS4" + rs.AwsSecurityCredentials.SecretAccessKey)
for _, signingInput := range []string{
dateStamp, rs.RegionName, serviceName, awsRequestType, stringToSign,
} {
signingKey, err = getHmacSha256(signingKey, []byte(signingInput))
if err != nil {
return "", err
}
}
return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, rs.AwsSecurityCredentials.AccessKeyID, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil
}
func getSha256(input []byte) (string, error) {
hash := sha256.New()
if _, err := hash.Write(input); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func getHmacSha256(key, input []byte) ([]byte, error) {
hash := hmac.New(sha256.New, key)
if _, err := hash.Write(input); err != nil {
return nil, err
}
return hash.Sum(nil), nil
}
func cloneRequest(r *http.Request) *http.Request {
r2 := new(http.Request)
*r2 = *r
if r.Header != nil {
r2.Header = make(http.Header, len(r.Header))
// Find total number of values.
headerCount := 0
for _, headerValues := range r.Header {
headerCount += len(headerValues)
}
copiedHeaders := make([]string, headerCount) // shared backing array for headers' values
for headerKey, headerValues := range r.Header {
headerCount = copy(copiedHeaders, headerValues)
r2.Header[headerKey] = copiedHeaders[:headerCount:headerCount]
copiedHeaders = copiedHeaders[headerCount:]
}
}
return r2
}
func canonicalPath(req *http.Request) string {
result := req.URL.EscapedPath()
if result == "" {
return "/"
}
return path.Clean(result)
}
func canonicalQuery(req *http.Request) string {
queryValues := req.URL.Query()
for queryKey := range queryValues {
sort.Strings(queryValues[queryKey])
}
return queryValues.Encode()
}
func canonicalHeaders(req *http.Request) (string, string) {
// Header keys need to be sorted alphabetically.
var headers []string
lowerCaseHeaders := make(http.Header)
for k, v := range req.Header {
k := strings.ToLower(k)
if _, ok := lowerCaseHeaders[k]; ok {
// include additional values
lowerCaseHeaders[k] = append(lowerCaseHeaders[k], v...)
} else {
headers = append(headers, k)
lowerCaseHeaders[k] = v
}
}
sort.Strings(headers)
var fullHeaders bytes.Buffer
for _, header := range headers {
headerValue := strings.Join(lowerCaseHeaders[header], ",")
fullHeaders.WriteString(header)
fullHeaders.WriteRune(':')
fullHeaders.WriteString(headerValue)
fullHeaders.WriteRune('\n')
}
return strings.Join(headers, ";"), fullHeaders.String()
}
func requestDataHash(req *http.Request) (string, error) {
var requestData []byte
if req.Body != nil {
requestBody, err := req.GetBody()
if err != nil {
return "", err
}
defer requestBody.Close()
requestData, err = internal.ReadAll(requestBody)
if err != nil {
return "", err
}
}
return getSha256(requestData)
}
func requestHost(req *http.Request) string {
if req.Host != "" {
return req.Host
}
return req.URL.Host
}
func canonicalRequest(req *http.Request, canonicalHeaderColumns, canonicalHeaderData string) (string, error) {
dataHash, err := requestDataHash(req)
if err != nil {
return "", err
}
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", req.Method, canonicalPath(req), canonicalQuery(req), canonicalHeaderData, canonicalHeaderColumns, dataHash), nil
}
type awsRequestHeader struct {
Key string `json:"key"`
Value string `json:"value"`
}
type awsRequest struct {
URL string `json:"url"`
Method string `json:"method"`
Headers []awsRequestHeader `json:"headers"`
}
// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is
// required.
func canRetrieveRegionFromEnvironment() bool {
return getenv(awsRegionEnvVar) != "" || getenv(awsDefaultRegionEnvVar) != ""
}
// Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available.
func canRetrieveSecurityCredentialFromEnvironment() bool {
return getenv(awsAccessKeyIDEnvVar) != "" && getenv(awsSecretAccessKeyEnvVar) != ""
}
func (sp *awsSubjectProvider) shouldUseMetadataServer() bool {
return sp.securityCredentialsProvider == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment())
}

View file

@ -0,0 +1,284 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"regexp"
"strings"
"time"
"cloud.google.com/go/auth/internal"
)
const (
executableSupportedMaxVersion = 1
executableDefaultTimeout = 30 * time.Second
executableSource = "response"
executableProviderType = "executable"
outputFileSource = "output file"
allowExecutablesEnvVar = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
idTokenType = "urn:ietf:params:oauth:token-type:id_token"
saml2TokenType = "urn:ietf:params:oauth:token-type:saml2"
)
var (
serviceAccountImpersonationRE = regexp.MustCompile(`https://iamcredentials..+/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken`)
)
type nonCacheableError struct {
message string
}
func (nce nonCacheableError) Error() string {
return nce.message
}
// environment is a contract for testing
type environment interface {
existingEnv() []string
getenv(string) string
run(ctx context.Context, command string, env []string) ([]byte, error)
now() time.Time
}
type runtimeEnvironment struct{}
func (r runtimeEnvironment) existingEnv() []string {
return os.Environ()
}
func (r runtimeEnvironment) getenv(key string) string {
return os.Getenv(key)
}
func (r runtimeEnvironment) now() time.Time {
return time.Now().UTC()
}
func (r runtimeEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
splitCommand := strings.Fields(command)
cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
cmd.Env = env
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return nil, context.DeadlineExceeded
}
if exitError, ok := err.(*exec.ExitError); ok {
return nil, exitCodeError(exitError)
}
return nil, executableError(err)
}
bytesStdout := bytes.TrimSpace(stdout.Bytes())
if len(bytesStdout) > 0 {
return bytesStdout, nil
}
return bytes.TrimSpace(stderr.Bytes()), nil
}
type executableSubjectProvider struct {
Command string
Timeout time.Duration
OutputFile string
client *http.Client
opts *Options
env environment
}
type executableResponse struct {
Version int `json:"version,omitempty"`
Success *bool `json:"success,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpirationTime int64 `json:"expiration_time,omitempty"`
IDToken string `json:"id_token,omitempty"`
SamlResponse string `json:"saml_response,omitempty"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (sp *executableSubjectProvider) parseSubjectTokenFromSource(response []byte, source string, now int64) (string, error) {
var result executableResponse
if err := json.Unmarshal(response, &result); err != nil {
return "", jsonParsingError(source, string(response))
}
// Validate
if result.Version == 0 {
return "", missingFieldError(source, "version")
}
if result.Success == nil {
return "", missingFieldError(source, "success")
}
if !*result.Success {
if result.Code == "" || result.Message == "" {
return "", malformedFailureError()
}
return "", userDefinedError(result.Code, result.Message)
}
if result.Version > executableSupportedMaxVersion || result.Version < 0 {
return "", unsupportedVersionError(source, result.Version)
}
if result.ExpirationTime == 0 && sp.OutputFile != "" {
return "", missingFieldError(source, "expiration_time")
}
if result.TokenType == "" {
return "", missingFieldError(source, "token_type")
}
if result.ExpirationTime != 0 && result.ExpirationTime < now {
return "", tokenExpiredError()
}
switch result.TokenType {
case jwtTokenType, idTokenType:
if result.IDToken == "" {
return "", missingFieldError(source, "id_token")
}
return result.IDToken, nil
case saml2TokenType:
if result.SamlResponse == "" {
return "", missingFieldError(source, "saml_response")
}
return result.SamlResponse, nil
default:
return "", tokenTypeError(source)
}
}
func (sp *executableSubjectProvider) subjectToken(ctx context.Context) (string, error) {
if token, err := sp.getTokenFromOutputFile(); token != "" || err != nil {
return token, err
}
return sp.getTokenFromExecutableCommand(ctx)
}
func (sp *executableSubjectProvider) providerType() string {
return executableProviderType
}
func (sp *executableSubjectProvider) getTokenFromOutputFile() (token string, err error) {
if sp.OutputFile == "" {
// This ExecutableCredentialSource doesn't use an OutputFile.
return "", nil
}
file, err := os.Open(sp.OutputFile)
if err != nil {
// No OutputFile found. Hasn't been created yet, so skip it.
return "", nil
}
defer file.Close()
data, err := internal.ReadAll(file)
if err != nil || len(data) == 0 {
// Cachefile exists, but no data found. Get new credential.
return "", nil
}
token, err = sp.parseSubjectTokenFromSource(data, outputFileSource, sp.env.now().Unix())
if err != nil {
if _, ok := err.(nonCacheableError); ok {
// If the cached token is expired we need a new token,
// and if the cache contains a failure, we need to try again.
return "", nil
}
// There was an error in the cached token, and the developer should be aware of it.
return "", err
}
// Token parsing succeeded. Use found token.
return token, nil
}
func (sp *executableSubjectProvider) executableEnvironment() []string {
result := sp.env.existingEnv()
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", sp.opts.Audience))
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", sp.opts.SubjectTokenType))
result = append(result, "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0")
if sp.opts.ServiceAccountImpersonationURL != "" {
matches := serviceAccountImpersonationRE.FindStringSubmatch(sp.opts.ServiceAccountImpersonationURL)
if matches != nil {
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=%v", matches[1]))
}
}
if sp.OutputFile != "" {
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", sp.OutputFile))
}
return result
}
func (sp *executableSubjectProvider) getTokenFromExecutableCommand(ctx context.Context) (string, error) {
// For security reasons, we need our consumers to set this environment variable to allow executables to be run.
if sp.env.getenv(allowExecutablesEnvVar) != "1" {
return "", errors.New("credentials: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
}
ctx, cancel := context.WithDeadline(ctx, sp.env.now().Add(sp.Timeout))
defer cancel()
output, err := sp.env.run(ctx, sp.Command, sp.executableEnvironment())
if err != nil {
return "", err
}
return sp.parseSubjectTokenFromSource(output, executableSource, sp.env.now().Unix())
}
func missingFieldError(source, field string) error {
return fmt.Errorf("credentials: %q missing %q field", source, field)
}
func jsonParsingError(source, data string) error {
return fmt.Errorf("credentials: unable to parse %q: %v", source, data)
}
func malformedFailureError() error {
return nonCacheableError{"credentials: response must include `error` and `message` fields when unsuccessful"}
}
func userDefinedError(code, message string) error {
return nonCacheableError{fmt.Sprintf("credentials: response contains unsuccessful response: (%v) %v", code, message)}
}
func unsupportedVersionError(source string, version int) error {
return fmt.Errorf("credentials: %v contains unsupported version: %v", source, version)
}
func tokenExpiredError() error {
return nonCacheableError{"credentials: the token returned by the executable is expired"}
}
func tokenTypeError(source string) error {
return fmt.Errorf("credentials: %v contains unsupported token type", source)
}
func exitCodeError(err *exec.ExitError) error {
return fmt.Errorf("credentials: executable command failed with exit code %v: %w", err.ExitCode(), err)
}
func executableError(err error) error {
return fmt.Errorf("credentials: executable command failed: %w", err)
}

View file

@ -0,0 +1,367 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"context"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/internal/impersonate"
"cloud.google.com/go/auth/credentials/internal/stsexchange"
"cloud.google.com/go/auth/internal/credsfile"
)
const (
timeoutMinimum = 5 * time.Second
timeoutMaximum = 120 * time.Second
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
defaultUniverseDomain = "googleapis.com"
)
var (
// Now aliases time.Now for testing
Now = func() time.Time {
return time.Now().UTC()
}
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
)
// Options stores the configuration for fetching tokens with external credentials.
type Options struct {
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
// identity pool or the workforce pool and the provider identifier in that pool.
Audience string
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
// e.g. `urn:ietf:params:oauth:token-type:jwt`.
SubjectTokenType string
// TokenURL is the STS token exchange endpoint.
TokenURL string
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
// user attributes like account identifier, eg. email, username, uid, etc). This is
// needed for gCloud session account identification.
TokenInfoURL string
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
ServiceAccountImpersonationURL string
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
// token will be valid for.
ServiceAccountImpersonationLifetimeSeconds int
// ClientSecret is currently only required if token_info endpoint also
// needs to be called with the generated GCP access token. When provided, STS will be
// called with additional basic authentication using client_id as username and client_secret as password.
ClientSecret string
// ClientID is only required in conjunction with ClientSecret, as described above.
ClientID string
// CredentialSource contains the necessary information to retrieve the token itself, as well
// as some environmental information.
CredentialSource *credsfile.CredentialSource
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
// will set the x-goog-user-project which overrides the project associated with the credentials.
QuotaProjectID string
// Scopes contains the desired scopes for the returned access token.
Scopes []string
// WorkforcePoolUserProject should be set when it is a workforce pool and
// not a workload identity pool. The underlying principal must still have
// serviceusage.services.use IAM permission to use the project for
// billing/quota. Optional.
WorkforcePoolUserProject string
// UniverseDomain is the default service domain for a given Cloud universe.
// This value will be used in the default STS token URL. The default value
// is "googleapis.com". It will not be used if TokenURL is set. Optional.
UniverseDomain string
// SubjectTokenProvider is an optional token provider for OIDC/SAML
// credentials. One of SubjectTokenProvider, AWSSecurityCredentialProvider
// or CredentialSource must be provided. Optional.
SubjectTokenProvider SubjectTokenProvider
// AwsSecurityCredentialsProvider is an AWS Security Credential provider
// for AWS credentials. One of SubjectTokenProvider,
// AWSSecurityCredentialProvider or CredentialSource must be provided. Optional.
AwsSecurityCredentialsProvider AwsSecurityCredentialsProvider
// Client for token request.
Client *http.Client
}
// SubjectTokenProvider can be used to supply a subject token to exchange for a
// GCP access token.
type SubjectTokenProvider interface {
// SubjectToken should return a valid subject token or an error.
// The external account token provider does not cache the returned subject
// token, so caching logic should be implemented in the provider to prevent
// multiple requests for the same subject token.
SubjectToken(ctx context.Context, opts *RequestOptions) (string, error)
}
// RequestOptions contains information about the requested subject token or AWS
// security credentials from the Google external account credential.
type RequestOptions struct {
// Audience is the requested audience for the external account credential.
Audience string
// Subject token type is the requested subject token type for the external
// account credential. Expected values include:
// “urn:ietf:params:oauth:token-type:jwt”
// “urn:ietf:params:oauth:token-type:id-token”
// “urn:ietf:params:oauth:token-type:saml2”
// “urn:ietf:params:aws:token-type:aws4_request”
SubjectTokenType string
}
// AwsSecurityCredentialsProvider can be used to supply AwsSecurityCredentials
// and an AWS Region to exchange for a GCP access token.
type AwsSecurityCredentialsProvider interface {
// AwsRegion should return the AWS region or an error.
AwsRegion(ctx context.Context, opts *RequestOptions) (string, error)
// GetAwsSecurityCredentials should return a valid set of
// AwsSecurityCredentials or an error. The external account token provider
// does not cache the returned security credentials, so caching logic should
// be implemented in the provider to prevent multiple requests for the
// same security credentials.
AwsSecurityCredentials(ctx context.Context, opts *RequestOptions) (*AwsSecurityCredentials, error)
}
// AwsSecurityCredentials models AWS security credentials.
type AwsSecurityCredentials struct {
// AccessKeyId is the AWS Access Key ID - Required.
AccessKeyID string `json:"AccessKeyID"`
// SecretAccessKey is the AWS Secret Access Key - Required.
SecretAccessKey string `json:"SecretAccessKey"`
// SessionToken is the AWS Session token. This should be provided for
// temporary AWS security credentials - Optional.
SessionToken string `json:"Token"`
}
func (o *Options) validate() error {
if o.Audience == "" {
return fmt.Errorf("externalaccount: Audience must be set")
}
if o.SubjectTokenType == "" {
return fmt.Errorf("externalaccount: Subject token type must be set")
}
if o.WorkforcePoolUserProject != "" {
if valid := validWorkforceAudiencePattern.MatchString(o.Audience); !valid {
return fmt.Errorf("externalaccount: workforce_pool_user_project should not be set for non-workforce pool credentials")
}
}
count := 0
if o.CredentialSource != nil {
count++
}
if o.SubjectTokenProvider != nil {
count++
}
if o.AwsSecurityCredentialsProvider != nil {
count++
}
if count == 0 {
return fmt.Errorf("externalaccount: one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
}
if count > 1 {
return fmt.Errorf("externalaccount: only one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
}
return nil
}
// resolveTokenURL sets the default STS token endpoint with the configured
// universe domain.
func (o *Options) resolveTokenURL() {
if o.TokenURL != "" {
return
} else if o.UniverseDomain != "" {
o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, o.UniverseDomain, 1)
} else {
o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
}
}
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
// configured with the provided options.
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
opts.resolveTokenURL()
stp, err := newSubjectTokenProvider(opts)
if err != nil {
return nil, err
}
tp := &tokenProvider{
client: opts.Client,
opts: opts,
stp: stp,
}
if opts.ServiceAccountImpersonationURL == "" {
return auth.NewCachedTokenProvider(tp, nil), nil
}
scopes := make([]string, len(opts.Scopes))
copy(scopes, opts.Scopes)
// needed for impersonation
tp.opts.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
imp, err := impersonate.NewTokenProvider(&impersonate.Options{
Client: opts.Client,
URL: opts.ServiceAccountImpersonationURL,
Scopes: scopes,
Tp: auth.NewCachedTokenProvider(tp, nil),
TokenLifetimeSeconds: opts.ServiceAccountImpersonationLifetimeSeconds,
})
if err != nil {
return nil, err
}
return auth.NewCachedTokenProvider(imp, nil), nil
}
type subjectTokenProvider interface {
subjectToken(ctx context.Context) (string, error)
providerType() string
}
// tokenProvider is the provider that handles external credentials. It is used to retrieve Tokens.
type tokenProvider struct {
client *http.Client
opts *Options
stp subjectTokenProvider
}
func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
subjectToken, err := tp.stp.subjectToken(ctx)
if err != nil {
return nil, err
}
stsRequest := &stsexchange.TokenRequest{
GrantType: stsexchange.GrantType,
Audience: tp.opts.Audience,
Scope: tp.opts.Scopes,
RequestedTokenType: stsexchange.TokenType,
SubjectToken: subjectToken,
SubjectTokenType: tp.opts.SubjectTokenType,
}
header := make(http.Header)
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Add("x-goog-api-client", getGoogHeaderValue(tp.opts, tp.stp))
clientAuth := stsexchange.ClientAuthentication{
AuthStyle: auth.StyleInHeader,
ClientID: tp.opts.ClientID,
ClientSecret: tp.opts.ClientSecret,
}
var options map[string]interface{}
// Do not pass workforce_pool_user_project when client authentication is used.
// The client ID is sufficient for determining the user project.
if tp.opts.WorkforcePoolUserProject != "" && tp.opts.ClientID == "" {
options = map[string]interface{}{
"userProject": tp.opts.WorkforcePoolUserProject,
}
}
stsResp, err := stsexchange.ExchangeToken(ctx, &stsexchange.Options{
Client: tp.client,
Endpoint: tp.opts.TokenURL,
Request: stsRequest,
Authentication: clientAuth,
Headers: header,
ExtraOpts: options,
})
if err != nil {
return nil, err
}
tok := &auth.Token{
Value: stsResp.AccessToken,
Type: stsResp.TokenType,
}
// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
if stsResp.ExpiresIn <= 0 {
return nil, fmt.Errorf("credentials: got invalid expiry from security token service")
}
tok.Expiry = Now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
return tok, nil
}
// newSubjectTokenProvider determines the type of credsfile.CredentialSource needed to create a
// subjectTokenProvider
func newSubjectTokenProvider(o *Options) (subjectTokenProvider, error) {
reqOpts := &RequestOptions{Audience: o.Audience, SubjectTokenType: o.SubjectTokenType}
if o.AwsSecurityCredentialsProvider != nil {
return &awsSubjectProvider{
securityCredentialsProvider: o.AwsSecurityCredentialsProvider,
TargetResource: o.Audience,
reqOpts: reqOpts,
}, nil
} else if o.SubjectTokenProvider != nil {
return &programmaticProvider{stp: o.SubjectTokenProvider, opts: reqOpts}, nil
} else if len(o.CredentialSource.EnvironmentID) > 3 && o.CredentialSource.EnvironmentID[:3] == "aws" {
if awsVersion, err := strconv.Atoi(o.CredentialSource.EnvironmentID[3:]); err == nil {
if awsVersion != 1 {
return nil, fmt.Errorf("credentials: aws version '%d' is not supported in the current build", awsVersion)
}
awsProvider := &awsSubjectProvider{
EnvironmentID: o.CredentialSource.EnvironmentID,
RegionURL: o.CredentialSource.RegionURL,
RegionalCredVerificationURL: o.CredentialSource.RegionalCredVerificationURL,
CredVerificationURL: o.CredentialSource.URL,
TargetResource: o.Audience,
Client: o.Client,
}
if o.CredentialSource.IMDSv2SessionTokenURL != "" {
awsProvider.IMDSv2SessionTokenURL = o.CredentialSource.IMDSv2SessionTokenURL
}
return awsProvider, nil
}
} else if o.CredentialSource.File != "" {
return &fileSubjectProvider{File: o.CredentialSource.File, Format: o.CredentialSource.Format}, nil
} else if o.CredentialSource.URL != "" {
return &urlSubjectProvider{URL: o.CredentialSource.URL, Headers: o.CredentialSource.Headers, Format: o.CredentialSource.Format, Client: o.Client}, nil
} else if o.CredentialSource.Executable != nil {
ec := o.CredentialSource.Executable
if ec.Command == "" {
return nil, errors.New("credentials: missing `command` field — executable command must be provided")
}
execProvider := &executableSubjectProvider{}
execProvider.Command = ec.Command
if ec.TimeoutMillis == 0 {
execProvider.Timeout = executableDefaultTimeout
} else {
execProvider.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond
if execProvider.Timeout < timeoutMinimum || execProvider.Timeout > timeoutMaximum {
return nil, fmt.Errorf("credentials: invalid `timeout_millis` field — executable timeout must be between %v and %v seconds", timeoutMinimum.Seconds(), timeoutMaximum.Seconds())
}
}
execProvider.OutputFile = ec.OutputFile
execProvider.client = o.Client
execProvider.opts = o
execProvider.env = runtimeEnvironment{}
return execProvider, nil
}
return nil, errors.New("credentials: unable to parse credential source")
}
func getGoogHeaderValue(conf *Options, p subjectTokenProvider) string {
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
goVersion(),
"unknown",
p.providerType(),
conf.ServiceAccountImpersonationURL != "",
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
}

View file

@ -0,0 +1,78 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
)
const (
fileProviderType = "file"
)
type fileSubjectProvider struct {
File string
Format *credsfile.Format
}
func (sp *fileSubjectProvider) subjectToken(context.Context) (string, error) {
tokenFile, err := os.Open(sp.File)
if err != nil {
return "", fmt.Errorf("credentials: failed to open credential file %q: %w", sp.File, err)
}
defer tokenFile.Close()
tokenBytes, err := internal.ReadAll(tokenFile)
if err != nil {
return "", fmt.Errorf("credentials: failed to read credential file: %w", err)
}
tokenBytes = bytes.TrimSpace(tokenBytes)
if sp.Format == nil {
return string(tokenBytes), nil
}
switch sp.Format.Type {
case fileTypeJSON:
jsonData := make(map[string]interface{})
err = json.Unmarshal(tokenBytes, &jsonData)
if err != nil {
return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
}
val, ok := jsonData[sp.Format.SubjectTokenFieldName]
if !ok {
return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
}
token, ok := val.(string)
if !ok {
return "", errors.New("credentials: improperly formatted subject token")
}
return token, nil
case fileTypeText:
return string(tokenBytes), nil
default:
return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
}
}
func (sp *fileSubjectProvider) providerType() string {
return fileProviderType
}

View file

@ -0,0 +1,74 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"runtime"
"strings"
"unicode"
)
var (
// version is a package internal global variable for testing purposes.
version = runtime.Version
)
// versionUnknown is only used when the runtime version cannot be determined.
const versionUnknown = "UNKNOWN"
// goVersion returns a Go runtime version derived from the runtime environment
// that is modified to be suitable for reporting in a header, meaning it has no
// whitespace. If it is unable to determine the Go runtime version, it returns
// versionUnknown.
func goVersion() string {
const develPrefix = "devel +"
s := version()
if strings.HasPrefix(s, develPrefix) {
s = s[len(develPrefix):]
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
return s
} else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
notSemverRune := func(r rune) bool {
return !strings.ContainsRune("0123456789.", r)
}
if strings.HasPrefix(s, "go1") {
s = s[2:]
var prerelease string
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
s, prerelease = s[:p], s[p:]
}
if strings.HasSuffix(s, ".") {
s += "0"
} else if strings.Count(s, ".") < 2 {
s += ".0"
}
if prerelease != "" {
// Some release candidates already have a dash in them.
if !strings.HasPrefix(prerelease, "-") {
prerelease = "-" + prerelease
}
s += prerelease
}
return s
}
return versionUnknown
}

View file

@ -0,0 +1,30 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import "context"
type programmaticProvider struct {
opts *RequestOptions
stp SubjectTokenProvider
}
func (pp *programmaticProvider) providerType() string {
return programmaticProviderType
}
func (pp *programmaticProvider) subjectToken(ctx context.Context) (string, error) {
return pp.stp.SubjectToken(ctx, pp.opts)
}

View file

@ -0,0 +1,93 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
)
const (
fileTypeText = "text"
fileTypeJSON = "json"
urlProviderType = "url"
programmaticProviderType = "programmatic"
)
type urlSubjectProvider struct {
URL string
Headers map[string]string
Format *credsfile.Format
Client *http.Client
}
func (sp *urlSubjectProvider) subjectToken(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", sp.URL, nil)
if err != nil {
return "", fmt.Errorf("credentials: HTTP request for URL-sourced credential failed: %w", err)
}
for key, val := range sp.Headers {
req.Header.Add(key, val)
}
resp, err := sp.Client.Do(req)
if err != nil {
return "", fmt.Errorf("credentials: invalid response when retrieving subject token: %w", err)
}
defer resp.Body.Close()
respBody, err := internal.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("credentials: invalid body in subject token URL query: %w", err)
}
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
return "", fmt.Errorf("credentials: status code %d: %s", c, respBody)
}
if sp.Format == nil {
return string(respBody), nil
}
switch sp.Format.Type {
case "json":
jsonData := make(map[string]interface{})
err = json.Unmarshal(respBody, &jsonData)
if err != nil {
return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
}
val, ok := jsonData[sp.Format.SubjectTokenFieldName]
if !ok {
return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
}
token, ok := val.(string)
if !ok {
return "", errors.New("credentials: improperly formatted subject token")
}
return token, nil
case fileTypeText:
return string(respBody), nil
default:
return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
}
}
func (sp *urlSubjectProvider) providerType() string {
return urlProviderType
}

View file

@ -0,0 +1,110 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccountuser
import (
"context"
"errors"
"net/http"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/internal/stsexchange"
"cloud.google.com/go/auth/internal"
)
// Options stores the configuration for fetching tokens with external authorized
// user credentials.
type Options struct {
// Audience is the Secure Token Service (STS) audience which contains the
// resource name for the workforce pool and the provider identifier in that
// pool.
Audience string
// RefreshToken is the OAuth 2.0 refresh token.
RefreshToken string
// TokenURL is the STS token exchange endpoint for refresh.
TokenURL string
// TokenInfoURL is the STS endpoint URL for token introspection. Optional.
TokenInfoURL string
// ClientID is only required in conjunction with ClientSecret, as described
// below.
ClientID string
// ClientSecret is currently only required if token_info endpoint also needs
// to be called with the generated a cloud access token. When provided, STS
// will be called with additional basic authentication using client_id as
// username and client_secret as password.
ClientSecret string
// Scopes contains the desired scopes for the returned access token.
Scopes []string
// Client for token request.
Client *http.Client
}
func (c *Options) validate() bool {
return c.ClientID != "" && c.ClientSecret != "" && c.RefreshToken != "" && c.TokenURL != ""
}
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
// configured with the provided options.
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
if !opts.validate() {
return nil, errors.New("credentials: invalid external_account_authorized_user configuration")
}
tp := &tokenProvider{
o: opts,
}
return auth.NewCachedTokenProvider(tp, nil), nil
}
type tokenProvider struct {
o *Options
}
func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
opts := tp.o
clientAuth := stsexchange.ClientAuthentication{
AuthStyle: auth.StyleInHeader,
ClientID: opts.ClientID,
ClientSecret: opts.ClientSecret,
}
headers := make(http.Header)
headers.Set("Content-Type", "application/x-www-form-urlencoded")
stsResponse, err := stsexchange.RefreshAccessToken(ctx, &stsexchange.Options{
Client: opts.Client,
Endpoint: opts.TokenURL,
RefreshToken: opts.RefreshToken,
Authentication: clientAuth,
Headers: headers,
})
if err != nil {
return nil, err
}
if stsResponse.ExpiresIn < 0 {
return nil, errors.New("credentials: invalid expiry from security token service")
}
// guarded by the wrapping with CachedTokenProvider
if stsResponse.RefreshToken != "" {
opts.RefreshToken = stsResponse.RefreshToken
}
return &auth.Token{
Value: stsResponse.AccessToken,
Expiry: time.Now().UTC().Add(time.Duration(stsResponse.ExpiresIn) * time.Second),
Type: internal.TokenTypeBearer,
}, nil
}

View file

@ -0,0 +1,182 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gdch
import (
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/auth/internal/jwt"
)
const (
// GrantType is the grant type for the token request.
GrantType = "urn:ietf:params:oauth:token-type:token-exchange"
requestTokenType = "urn:ietf:params:oauth:token-type:access_token"
subjectTokenType = "urn:k8s:params:oauth:token-type:serviceaccount"
)
var (
gdchSupportFormatVersions map[string]bool = map[string]bool{
"1": true,
}
)
// Options for [NewTokenProvider].
type Options struct {
STSAudience string
Client *http.Client
}
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider] from a
// GDCH cred file.
func NewTokenProvider(f *credsfile.GDCHServiceAccountFile, o *Options) (auth.TokenProvider, error) {
if !gdchSupportFormatVersions[f.FormatVersion] {
return nil, fmt.Errorf("credentials: unsupported gdch_service_account format %q", f.FormatVersion)
}
if o.STSAudience == "" {
return nil, errors.New("credentials: STSAudience must be set for the GDCH auth flows")
}
pk, err := internal.ParseKey([]byte(f.PrivateKey))
if err != nil {
return nil, err
}
certPool, err := loadCertPool(f.CertPath)
if err != nil {
return nil, err
}
tp := gdchProvider{
serviceIdentity: fmt.Sprintf("system:serviceaccount:%s:%s", f.Project, f.Name),
tokenURL: f.TokenURL,
aud: o.STSAudience,
pk: pk,
pkID: f.PrivateKeyID,
certPool: certPool,
client: o.Client,
}
return tp, nil
}
func loadCertPool(path string) (*x509.CertPool, error) {
pool := x509.NewCertPool()
pem, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("credentials: failed to read certificate: %w", err)
}
pool.AppendCertsFromPEM(pem)
return pool, nil
}
type gdchProvider struct {
serviceIdentity string
tokenURL string
aud string
pk *rsa.PrivateKey
pkID string
certPool *x509.CertPool
client *http.Client
}
func (g gdchProvider) Token(ctx context.Context) (*auth.Token, error) {
addCertToTransport(g.client, g.certPool)
iat := time.Now()
exp := iat.Add(time.Hour)
claims := jwt.Claims{
Iss: g.serviceIdentity,
Sub: g.serviceIdentity,
Aud: g.tokenURL,
Iat: iat.Unix(),
Exp: exp.Unix(),
}
h := jwt.Header{
Algorithm: jwt.HeaderAlgRSA256,
Type: jwt.HeaderType,
KeyID: string(g.pkID),
}
payload, err := jwt.EncodeJWS(&h, &claims, g.pk)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("grant_type", GrantType)
v.Set("audience", g.aud)
v.Set("requested_token_type", requestTokenType)
v.Set("subject_token", payload)
v.Set("subject_token_type", subjectTokenType)
resp, err := g.client.PostForm(g.tokenURL, v)
if err != nil {
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
}
defer resp.Body.Close()
body, err := internal.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
}
if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
return nil, &auth.Error{
Response: resp,
Body: body,
}
}
var tokenRes struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return nil, fmt.Errorf("credentials: cannot fetch token: %w", err)
}
token := &auth.Token{
Value: tokenRes.AccessToken,
Type: tokenRes.TokenType,
}
raw := make(map[string]interface{})
json.Unmarshal(body, &raw) // no error checks for optional fields
token.Metadata = raw
if secs := tokenRes.ExpiresIn; secs > 0 {
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
}
return token, nil
}
// addCertToTransport makes a best effort attempt at adding in the cert info to
// the client. It tries to keep all configured transport settings if the
// underlying transport is an http.Transport. Or else it overwrites the
// transport with defaults adding in the certs.
func addCertToTransport(hc *http.Client, certPool *x509.CertPool) {
trans, ok := hc.Transport.(*http.Transport)
if !ok {
trans = http.DefaultTransport.(*http.Transport).Clone()
}
trans.TLSClientConfig = &tls.Config{
RootCAs: certPool,
}
}

View file

@ -0,0 +1,151 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package impersonate
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
)
const (
defaultTokenLifetime = "3600s"
authHeaderKey = "Authorization"
)
// generateAccesstokenReq is used for service account impersonation
type generateAccessTokenReq struct {
Delegates []string `json:"delegates,omitempty"`
Lifetime string `json:"lifetime,omitempty"`
Scope []string `json:"scope,omitempty"`
}
type impersonateTokenResponse struct {
AccessToken string `json:"accessToken"`
ExpireTime string `json:"expireTime"`
}
// NewTokenProvider uses a source credential, stored in Ts, to request an access token to the provided URL.
// Scopes can be defined when the access token is requested.
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
return opts, nil
}
// Options for [NewTokenProvider].
type Options struct {
// Tp is the source credential used to generate a token on the
// impersonated service account. Required.
Tp auth.TokenProvider
// URL is the endpoint to call to generate a token
// on behalf of the service account. Required.
URL string
// Scopes that the impersonated credential should have. Required.
Scopes []string
// Delegates are the service account email addresses in a delegation chain.
// Each service account must be granted roles/iam.serviceAccountTokenCreator
// on the next service account in the chain. Optional.
Delegates []string
// TokenLifetimeSeconds is the number of seconds the impersonation token will
// be valid for. Defaults to 1 hour if unset. Optional.
TokenLifetimeSeconds int
// Client configures the underlying client used to make network requests
// when fetching tokens. Required.
Client *http.Client
}
func (o *Options) validate() error {
if o.Tp == nil {
return errors.New("credentials: missing required 'source_credentials' field in impersonated credentials")
}
if o.URL == "" {
return errors.New("credentials: missing required 'service_account_impersonation_url' field in impersonated credentials")
}
return nil
}
// Token performs the exchange to get a temporary service account token to allow access to GCP.
func (o *Options) Token(ctx context.Context) (*auth.Token, error) {
lifetime := defaultTokenLifetime
if o.TokenLifetimeSeconds != 0 {
lifetime = fmt.Sprintf("%ds", o.TokenLifetimeSeconds)
}
reqBody := generateAccessTokenReq{
Lifetime: lifetime,
Scope: o.Scopes,
Delegates: o.Delegates,
}
b, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("credentials: unable to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", o.URL, bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("credentials: unable to create impersonation request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if err := setAuthHeader(ctx, o.Tp, req); err != nil {
return nil, err
}
resp, err := o.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("credentials: unable to generate access token: %w", err)
}
defer resp.Body.Close()
body, err := internal.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("credentials: unable to read body: %w", err)
}
if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
}
var accessTokenResp impersonateTokenResponse
if err := json.Unmarshal(body, &accessTokenResp); err != nil {
return nil, fmt.Errorf("credentials: unable to parse response: %w", err)
}
expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
if err != nil {
return nil, fmt.Errorf("credentials: unable to parse expiry: %w", err)
}
return &auth.Token{
Value: accessTokenResp.AccessToken,
Expiry: expiry,
Type: internal.TokenTypeBearer,
}, nil
}
func setAuthHeader(ctx context.Context, tp auth.TokenProvider, r *http.Request) error {
t, err := tp.Token(ctx)
if err != nil {
return err
}
typ := t.Type
if typ == "" {
typ = internal.TokenTypeBearer
}
r.Header.Set(authHeaderKey, typ+" "+t.Value)
return nil
}

View file

@ -0,0 +1,167 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stsexchange
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
)
const (
// GrantType for a sts exchange.
GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
// TokenType for a sts exchange.
TokenType = "urn:ietf:params:oauth:token-type:access_token"
jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
)
// Options stores the configuration for making an sts exchange request.
type Options struct {
Client *http.Client
Endpoint string
Request *TokenRequest
Authentication ClientAuthentication
Headers http.Header
// ExtraOpts are optional fields marshalled into the `options` field of the
// request body.
ExtraOpts map[string]interface{}
RefreshToken string
}
// RefreshAccessToken performs the token exchange using a refresh token flow.
func RefreshAccessToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("refresh_token", opts.RefreshToken)
return doRequest(ctx, opts, data)
}
// ExchangeToken performs an oauth2 token exchange with the provided endpoint.
func ExchangeToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
data := url.Values{}
data.Set("audience", opts.Request.Audience)
data.Set("grant_type", GrantType)
data.Set("requested_token_type", TokenType)
data.Set("subject_token_type", opts.Request.SubjectTokenType)
data.Set("subject_token", opts.Request.SubjectToken)
data.Set("scope", strings.Join(opts.Request.Scope, " "))
if opts.ExtraOpts != nil {
opts, err := json.Marshal(opts.ExtraOpts)
if err != nil {
return nil, fmt.Errorf("credentials: failed to marshal additional options: %w", err)
}
data.Set("options", string(opts))
}
return doRequest(ctx, opts, data)
}
func doRequest(ctx context.Context, opts *Options, data url.Values) (*TokenResponse, error) {
opts.Authentication.InjectAuthentication(data, opts.Headers)
encodedData := data.Encode()
req, err := http.NewRequestWithContext(ctx, "POST", opts.Endpoint, strings.NewReader(encodedData))
if err != nil {
return nil, fmt.Errorf("credentials: failed to properly build http request: %w", err)
}
for key, list := range opts.Headers {
for _, val := range list {
req.Header.Add(key, val)
}
}
req.Header.Set("Content-Length", strconv.Itoa(len(encodedData)))
resp, err := opts.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("credentials: invalid response from Secure Token Server: %w", err)
}
defer resp.Body.Close()
body, err := internal.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
}
var stsResp TokenResponse
if err := json.Unmarshal(body, &stsResp); err != nil {
return nil, fmt.Errorf("credentials: failed to unmarshal response body from Secure Token Server: %w", err)
}
return &stsResp, nil
}
// TokenRequest contains fields necessary to make an oauth2 token
// exchange.
type TokenRequest struct {
ActingParty struct {
ActorToken string
ActorTokenType string
}
GrantType string
Resource string
Audience string
Scope []string
RequestedTokenType string
SubjectToken string
SubjectTokenType string
}
// TokenResponse is used to decode the remote server response during
// an oauth2 token exchange.
type TokenResponse struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
}
// ClientAuthentication represents an OAuth client ID and secret and the
// mechanism for passing these credentials as stated in rfc6749#2.3.1.
type ClientAuthentication struct {
AuthStyle auth.Style
ClientID string
ClientSecret string
}
// InjectAuthentication is used to add authentication to a Secure Token Service
// exchange request. It modifies either the passed url.Values or http.Header
// depending on the desired authentication format.
func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) {
if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil {
return
}
switch c.AuthStyle {
case auth.StyleInHeader:
plainHeader := c.ClientID + ":" + c.ClientSecret
headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(plainHeader)))
default:
values.Set("client_id", c.ClientID)
values.Set("client_secret", c.ClientSecret)
}
}

View file

@ -0,0 +1,81 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credentials
import (
"context"
"crypto/rsa"
"fmt"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/auth/internal/jwt"
)
var (
// for testing
now func() time.Time = time.Now
)
// configureSelfSignedJWT uses the private key in the service account to create
// a JWT without making a network call.
func configureSelfSignedJWT(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) {
pk, err := internal.ParseKey([]byte(f.PrivateKey))
if err != nil {
return nil, fmt.Errorf("credentials: could not parse key: %w", err)
}
return &selfSignedTokenProvider{
email: f.ClientEmail,
audience: opts.Audience,
scopes: opts.scopes(),
pk: pk,
pkID: f.PrivateKeyID,
}, nil
}
type selfSignedTokenProvider struct {
email string
audience string
scopes []string
pk *rsa.PrivateKey
pkID string
}
func (tp *selfSignedTokenProvider) Token(context.Context) (*auth.Token, error) {
iat := now()
exp := iat.Add(time.Hour)
scope := strings.Join(tp.scopes, " ")
c := &jwt.Claims{
Iss: tp.email,
Sub: tp.email,
Aud: tp.audience,
Scope: scope,
Iat: iat.Unix(),
Exp: exp.Unix(),
}
h := &jwt.Header{
Algorithm: jwt.HeaderAlgRSA256,
Type: jwt.HeaderType,
KeyID: string(tp.pkID),
}
msg, err := jwt.EncodeJWS(h, c, tp.pk)
if err != nil {
return nil, fmt.Errorf("credentials: could not encode JWT: %w", err)
}
return &auth.Token{Value: msg, Type: internal.TokenTypeBearer, Expiry: exp}, nil
}

View file

@ -0,0 +1,62 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
// +build linux
package grpctransport
import (
"context"
"net"
"syscall"
"google.golang.org/grpc"
)
const (
// defaultTCPUserTimeout is the default TCP_USER_TIMEOUT socket option. By
// default is 20 seconds.
tcpUserTimeoutMilliseconds = 20000
// Copied from golang.org/x/sys/unix.TCP_USER_TIMEOUT.
tcpUserTimeoutOp = 0x12
)
func init() {
// timeoutDialerOption is a grpc.DialOption that contains dialer with
// socket option TCP_USER_TIMEOUT. This dialer requires go versions 1.11+.
timeoutDialerOption = grpc.WithContextDialer(dialTCPUserTimeout)
}
func dialTCPUserTimeout(ctx context.Context, addr string) (net.Conn, error) {
control := func(network, address string, c syscall.RawConn) error {
var syscallErr error
controlErr := c.Control(func(fd uintptr) {
syscallErr = syscall.SetsockoptInt(
int(fd), syscall.IPPROTO_TCP, tcpUserTimeoutOp, tcpUserTimeoutMilliseconds)
})
if syscallErr != nil {
return syscallErr
}
if controlErr != nil {
return controlErr
}
return nil
}
d := &net.Dialer{
Control: control,
}
return d.DialContext(ctx, "tcp", addr)
}

View file

@ -0,0 +1,123 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grpctransport
import (
"context"
"net"
"os"
"strconv"
"strings"
"cloud.google.com/go/auth"
"cloud.google.com/go/compute/metadata"
"google.golang.org/grpc"
grpcgoogle "google.golang.org/grpc/credentials/google"
)
func isDirectPathEnabled(endpoint string, opts *Options) bool {
if opts.InternalOptions != nil && !opts.InternalOptions.EnableDirectPath {
return false
}
if !checkDirectPathEndPoint(endpoint) {
return false
}
if b, _ := strconv.ParseBool(os.Getenv(disableDirectPathEnvVar)); b {
return false
}
return true
}
func checkDirectPathEndPoint(endpoint string) bool {
// Only [dns:///]host[:port] is supported, not other schemes (e.g., "tcp://" or "unix://").
// Also don't try direct path if the user has chosen an alternate name resolver
// (i.e., via ":///" prefix).
if strings.Contains(endpoint, "://") && !strings.HasPrefix(endpoint, "dns:///") {
return false
}
if endpoint == "" {
return false
}
return true
}
func isTokenProviderDirectPathCompatible(tp auth.TokenProvider, _ *Options) bool {
if tp == nil {
return false
}
tok, err := tp.Token(context.Background())
if err != nil {
return false
}
if tok == nil {
return false
}
if source, _ := tok.Metadata["auth.google.tokenSource"].(string); source != "compute-metadata" {
return false
}
if acct, _ := tok.Metadata["auth.google.serviceAccount"].(string); acct != "default" {
return false
}
return true
}
func isDirectPathXdsUsed(o *Options) bool {
// Method 1: Enable DirectPath xDS by env;
if b, _ := strconv.ParseBool(os.Getenv(enableDirectPathXdsEnvVar)); b {
return true
}
// Method 2: Enable DirectPath xDS by option;
if o.InternalOptions != nil && o.InternalOptions.EnableDirectPathXds {
return true
}
return false
}
// configureDirectPath returns some dial options and an endpoint to use if the
// configuration allows the use of direct path. If it does not the provided
// grpcOpts and endpoint are returned.
func configureDirectPath(grpcOpts []grpc.DialOption, opts *Options, endpoint string, creds *auth.Credentials) ([]grpc.DialOption, string) {
if isDirectPathEnabled(endpoint, opts) && metadata.OnGCE() && isTokenProviderDirectPathCompatible(creds, opts) {
// Overwrite all of the previously specific DialOptions, DirectPath uses its own set of credentials and certificates.
grpcOpts = []grpc.DialOption{
grpc.WithCredentialsBundle(grpcgoogle.NewDefaultCredentialsWithOptions(grpcgoogle.DefaultCredentialsOptions{PerRPCCreds: &grpcCredentialsProvider{creds: creds}}))}
if timeoutDialerOption != nil {
grpcOpts = append(grpcOpts, timeoutDialerOption)
}
// Check if google-c2p resolver is enabled for DirectPath
if isDirectPathXdsUsed(opts) {
// google-c2p resolver target must not have a port number
if addr, _, err := net.SplitHostPort(endpoint); err == nil {
endpoint = "google-c2p:///" + addr
} else {
endpoint = "google-c2p:///" + endpoint
}
} else {
if !strings.HasPrefix(endpoint, "dns:///") {
endpoint = "dns:///" + endpoint
}
grpcOpts = append(grpcOpts,
// For now all DirectPath go clients will be using the following lb config, but in future
// when different services need different configs, then we should change this to a
// per-service config.
grpc.WithDisableServiceConfig(),
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"grpclb":{"childPolicy":[{"pick_first":{}}]}}]}`))
}
// TODO: add support for system parameters (quota project, request reason) via chained interceptor.
}
return grpcOpts, endpoint
}

View file

@ -0,0 +1,308 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grpctransport
import (
"context"
"errors"
"fmt"
"net/http"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/transport"
"go.opencensus.io/plugin/ocgrpc"
"google.golang.org/grpc"
grpccreds "google.golang.org/grpc/credentials"
grpcinsecure "google.golang.org/grpc/credentials/insecure"
)
const (
// Check env to disable DirectPath traffic.
disableDirectPathEnvVar = "GOOGLE_CLOUD_DISABLE_DIRECT_PATH"
// Check env to decide if using google-c2p resolver for DirectPath traffic.
enableDirectPathXdsEnvVar = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS"
quotaProjectHeaderKey = "X-Goog-User-Project"
)
var (
// Set at init time by dial_socketopt.go. If nil, socketopt is not supported.
timeoutDialerOption grpc.DialOption
)
// Options used to configure a [GRPCClientConnPool] from [Dial].
type Options struct {
// DisableTelemetry disables default telemetry (OpenCensus). An example
// reason to do so would be to bind custom telemetry that overrides the
// defaults.
DisableTelemetry bool
// DisableAuthentication specifies that no authentication should be used. It
// is suitable only for testing and for accessing public resources, like
// public Google Cloud Storage buckets.
DisableAuthentication bool
// Endpoint overrides the default endpoint to be used for a service.
Endpoint string
// Metadata is extra gRPC metadata that will be appended to every outgoing
// request.
Metadata map[string]string
// GRPCDialOpts are dial options that will be passed to `grpc.Dial` when
// establishing a`grpc.Conn``
GRPCDialOpts []grpc.DialOption
// PoolSize is specifies how many connections to balance between when making
// requests. If unset or less than 1, the value defaults to 1.
PoolSize int
// Credentials used to add Authorization metadata to all requests. If set
// DetectOpts are ignored.
Credentials *auth.Credentials
// DetectOpts configures settings for detect Application Default
// Credentials.
DetectOpts *credentials.DetectOptions
// UniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com". This is the universe domain
// configured for the client, which will be compared to the universe domain
// that is separately configured for the credentials.
UniverseDomain string
// InternalOptions are NOT meant to be set directly by consumers of this
// package, they should only be set by generated client code.
InternalOptions *InternalOptions
}
// client returns the client a user set for the detect options or nil if one was
// not set.
func (o *Options) client() *http.Client {
if o.DetectOpts != nil && o.DetectOpts.Client != nil {
return o.DetectOpts.Client
}
return nil
}
func (o *Options) validate() error {
if o == nil {
return errors.New("grpctransport: opts required to be non-nil")
}
hasCreds := o.Credentials != nil ||
(o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) ||
(o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "")
if o.DisableAuthentication && hasCreds {
return errors.New("grpctransport: DisableAuthentication is incompatible with options that set or detect credentials")
}
return nil
}
func (o *Options) resolveDetectOptions() *credentials.DetectOptions {
io := o.InternalOptions
// soft-clone these so we are not updating a ref the user holds and may reuse
do := transport.CloneDetectOptions(o.DetectOpts)
// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
if (io != nil && io.EnableJWTWithScope) || do.Audience != "" {
do.UseSelfSignedJWT = true
}
// Only default scopes if user did not also set an audience.
if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 {
do.Scopes = make([]string, len(io.DefaultScopes))
copy(do.Scopes, io.DefaultScopes)
}
if len(do.Scopes) == 0 && do.Audience == "" && io != nil {
do.Audience = o.InternalOptions.DefaultAudience
}
return do
}
// InternalOptions are only meant to be set by generated client code. These are
// not meant to be set directly by consumers of this package. Configuration in
// this type is considered EXPERIMENTAL and may be removed at any time in the
// future without warning.
type InternalOptions struct {
// EnableNonDefaultSAForDirectPath overrides the default requirement for
// using the default service account for DirectPath.
EnableNonDefaultSAForDirectPath bool
// EnableDirectPath overrides the default attempt to use DirectPath.
EnableDirectPath bool
// EnableDirectPathXds overrides the default DirectPath type. It is only
// valid when DirectPath is enabled.
EnableDirectPathXds bool
// EnableJWTWithScope specifies if scope can be used with self-signed JWT.
EnableJWTWithScope bool
// DefaultAudience specifies a default audience to be used as the audience
// field ("aud") for the JWT token authentication.
DefaultAudience string
// DefaultEndpointTemplate combined with UniverseDomain specifies
// the default endpoint.
DefaultEndpointTemplate string
// DefaultMTLSEndpoint specifies the default mTLS endpoint.
DefaultMTLSEndpoint string
// DefaultScopes specifies the default OAuth2 scopes to be used for a
// service.
DefaultScopes []string
}
// Dial returns a GRPCClientConnPool that can be used to communicate with a
// Google cloud service, configured with the provided [Options]. It
// automatically appends Authorization metadata to all outgoing requests.
func Dial(ctx context.Context, secure bool, opts *Options) (GRPCClientConnPool, error) {
if err := opts.validate(); err != nil {
return nil, err
}
if opts.PoolSize <= 1 {
conn, err := dial(ctx, secure, opts)
if err != nil {
return nil, err
}
return &singleConnPool{conn}, nil
}
pool := &roundRobinConnPool{}
for i := 0; i < opts.PoolSize; i++ {
conn, err := dial(ctx, false, opts)
if err != nil {
// ignore close error, if any
defer pool.Close()
return nil, err
}
pool.conns = append(pool.conns, conn)
}
return pool, nil
}
// return a GRPCClientConnPool if pool == 1 or else a pool of of them if >1
func dial(ctx context.Context, secure bool, opts *Options) (*grpc.ClientConn, error) {
tOpts := &transport.Options{
Endpoint: opts.Endpoint,
Client: opts.client(),
UniverseDomain: opts.UniverseDomain,
}
if io := opts.InternalOptions; io != nil {
tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
}
transportCreds, endpoint, err := transport.GetGRPCTransportCredsAndEndpoint(tOpts)
if err != nil {
return nil, err
}
if !secure {
transportCreds = grpcinsecure.NewCredentials()
}
// Initialize gRPC dial options with transport-level security options.
grpcOpts := []grpc.DialOption{
grpc.WithTransportCredentials(transportCreds),
}
// Authentication can only be sent when communicating over a secure connection.
if !opts.DisableAuthentication {
metadata := opts.Metadata
creds, err := credentials.DetectDefault(opts.resolveDetectOptions())
if err != nil {
return nil, err
}
if opts.Credentials != nil {
creds = opts.Credentials
}
qp, err := creds.QuotaProjectID(ctx)
if err != nil {
return nil, err
}
if qp != "" {
if metadata == nil {
metadata = make(map[string]string, 1)
}
metadata[quotaProjectHeaderKey] = qp
}
grpcOpts = append(grpcOpts,
grpc.WithPerRPCCredentials(&grpcCredentialsProvider{
creds: creds,
metadata: metadata,
clientUniverseDomain: opts.UniverseDomain,
}),
)
// Attempt Direct Path
grpcOpts, endpoint = configureDirectPath(grpcOpts, opts, endpoint, creds)
}
// Add tracing, but before the other options, so that clients can override the
// gRPC stats handler.
// This assumes that gRPC options are processed in order, left to right.
grpcOpts = addOCStatsHandler(grpcOpts, opts)
grpcOpts = append(grpcOpts, opts.GRPCDialOpts...)
return grpc.DialContext(ctx, endpoint, grpcOpts...)
}
// grpcCredentialsProvider satisfies https://pkg.go.dev/google.golang.org/grpc/credentials#PerRPCCredentials.
type grpcCredentialsProvider struct {
creds *auth.Credentials
secure bool
// Additional metadata attached as headers.
metadata map[string]string
clientUniverseDomain string
}
// getClientUniverseDomain returns the default service domain for a given Cloud universe.
// The default value is "googleapis.com". This is the universe domain
// configured for the client, which will be compared to the universe domain
// that is separately configured for the credentials.
func (c *grpcCredentialsProvider) getClientUniverseDomain() string {
if c.clientUniverseDomain == "" {
return internal.DefaultUniverseDomain
}
return c.clientUniverseDomain
}
func (c *grpcCredentialsProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
credentialsUniverseDomain, err := c.creds.UniverseDomain(ctx)
if err != nil {
return nil, err
}
if err := transport.ValidateUniverseDomain(c.getClientUniverseDomain(), credentialsUniverseDomain); err != nil {
return nil, err
}
token, err := c.creds.Token(ctx)
if err != nil {
return nil, err
}
if c.secure {
ri, _ := grpccreds.RequestInfoFromContext(ctx)
if err = grpccreds.CheckSecurityLevel(ri.AuthInfo, grpccreds.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("unable to transfer credentials PerRPCCredentials: %v", err)
}
}
metadata := map[string]string{
"authorization": token.Type + " " + token.Value,
}
for k, v := range c.metadata {
metadata[k] = v
}
return metadata, nil
}
func (c *grpcCredentialsProvider) RequireTransportSecurity() bool {
return c.secure
}
func addOCStatsHandler(dialOpts []grpc.DialOption, opts *Options) []grpc.DialOption {
if opts.DisableTelemetry {
return dialOpts
}
return append(dialOpts, grpc.WithStatsHandler(&ocgrpc.ClientHandler{}))
}

119
vendor/cloud.google.com/go/auth/grpctransport/pool.go generated vendored Normal file
View file

@ -0,0 +1,119 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package grpctransport
import (
"context"
"fmt"
"sync/atomic"
"google.golang.org/grpc"
)
// GRPCClientConnPool is an interface that satisfies
// [google.golang.org/grpc.ClientConnInterface] and has some utility functions
// that are needed for connection lifecycle when using in a client library. It
// may be a pool or a single connection. This interface is not intended to, and
// can't be, implemented by others.
type GRPCClientConnPool interface {
// Connection returns a [google.golang.org/grpc.ClientConn] from the pool.
//
// ClientConn aren't returned to the pool and should not be closed directly.
Connection() *grpc.ClientConn
// Len returns the number of connections in the pool. It will always return
// the same value.
Len() int
// Close closes every ClientConn in the pool. The error returned by Close
// may be a single error or multiple errors.
Close() error
grpc.ClientConnInterface
// private ensure others outside this package can't implement this type
private()
}
// singleConnPool is a special case for a single connection.
type singleConnPool struct {
*grpc.ClientConn
}
func (p *singleConnPool) Connection() *grpc.ClientConn { return p.ClientConn }
func (p *singleConnPool) Len() int { return 1 }
func (p *singleConnPool) private() {}
type roundRobinConnPool struct {
conns []*grpc.ClientConn
idx uint32 // access via sync/atomic
}
func (p *roundRobinConnPool) Len() int {
return len(p.conns)
}
func (p *roundRobinConnPool) Connection() *grpc.ClientConn {
i := atomic.AddUint32(&p.idx, 1)
return p.conns[i%uint32(len(p.conns))]
}
func (p *roundRobinConnPool) Close() error {
var errs multiError
for _, conn := range p.conns {
if err := conn.Close(); err != nil {
errs = append(errs, err)
}
}
if len(errs) == 0 {
return nil
}
return errs
}
func (p *roundRobinConnPool) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
return p.Connection().Invoke(ctx, method, args, reply, opts...)
}
func (p *roundRobinConnPool) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return p.Connection().NewStream(ctx, desc, method, opts...)
}
func (p *roundRobinConnPool) private() {}
// multiError represents errors from multiple conns in the group.
type multiError []error
func (m multiError) Error() string {
s, n := "", 0
for _, e := range m {
if e != nil {
if n == 0 {
s = e.Error()
}
n++
}
}
switch n {
case 0:
return "(0 errors)"
case 1:
return s
case 2:
return s + " (and 1 other error)"
}
return fmt.Sprintf("%s (and %d other errors)", s, n-1)
}

View file

@ -0,0 +1,195 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httptransport
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"cloud.google.com/go/auth"
detect "cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/transport"
)
// ClientCertProvider is a function that returns a TLS client certificate to be
// used when opening TLS connections. It follows the same semantics as
// [crypto/tls.Config.GetClientCertificate].
type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
// Options used to configure a [net/http.Client] from [NewClient].
type Options struct {
// DisableTelemetry disables default telemetry (OpenCensus). An example
// reason to do so would be to bind custom telemetry that overrides the
// defaults.
DisableTelemetry bool
// DisableAuthentication specifies that no authentication should be used. It
// is suitable only for testing and for accessing public resources, like
// public Google Cloud Storage buckets.
DisableAuthentication bool
// Headers are extra HTTP headers that will be appended to every outgoing
// request.
Headers http.Header
// Endpoint overrides the default endpoint to be used for a service.
Endpoint string
// APIKey specifies an API key to be used as the basis for authentication.
// If set DetectOpts are ignored.
APIKey string
// Credentials used to add Authorization header to all requests. If set
// DetectOpts are ignored.
Credentials *auth.Credentials
// ClientCertProvider is a function that returns a TLS client certificate to
// be used when opening TLS connections. It follows the same semantics as
// crypto/tls.Config.GetClientCertificate.
ClientCertProvider ClientCertProvider
// DetectOpts configures settings for detect Application Default
// Credentials.
DetectOpts *detect.DetectOptions
// UniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com". This is the universe domain
// configured for the client, which will be compared to the universe domain
// that is separately configured for the credentials.
UniverseDomain string
// InternalOptions are NOT meant to be set directly by consumers of this
// package, they should only be set by generated client code.
InternalOptions *InternalOptions
}
func (o *Options) validate() error {
if o == nil {
return errors.New("httptransport: opts required to be non-nil")
}
hasCreds := o.APIKey != "" ||
o.Credentials != nil ||
(o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) ||
(o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "")
if o.DisableAuthentication && hasCreds {
return errors.New("httptransport: DisableAuthentication is incompatible with options that set or detect credentials")
}
return nil
}
// client returns the client a user set for the detect options or nil if one was
// not set.
func (o *Options) client() *http.Client {
if o.DetectOpts != nil && o.DetectOpts.Client != nil {
return o.DetectOpts.Client
}
return nil
}
func (o *Options) resolveDetectOptions() *detect.DetectOptions {
io := o.InternalOptions
// soft-clone these so we are not updating a ref the user holds and may reuse
do := transport.CloneDetectOptions(o.DetectOpts)
// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
if (io != nil && io.EnableJWTWithScope) || do.Audience != "" {
do.UseSelfSignedJWT = true
}
// Only default scopes if user did not also set an audience.
if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 {
do.Scopes = make([]string, len(io.DefaultScopes))
copy(do.Scopes, io.DefaultScopes)
}
if len(do.Scopes) == 0 && do.Audience == "" && io != nil {
do.Audience = o.InternalOptions.DefaultAudience
}
return do
}
// InternalOptions are only meant to be set by generated client code. These are
// not meant to be set directly by consumers of this package. Configuration in
// this type is considered EXPERIMENTAL and may be removed at any time in the
// future without warning.
type InternalOptions struct {
// EnableJWTWithScope specifies if scope can be used with self-signed JWT.
EnableJWTWithScope bool
// DefaultAudience specifies a default audience to be used as the audience
// field ("aud") for the JWT token authentication.
DefaultAudience string
// DefaultEndpointTemplate combined with UniverseDomain specifies the
// default endpoint.
DefaultEndpointTemplate string
// DefaultMTLSEndpoint specifies the default mTLS endpoint.
DefaultMTLSEndpoint string
// DefaultScopes specifies the default OAuth2 scopes to be used for a
// service.
DefaultScopes []string
}
// AddAuthorizationMiddleware adds a middleware to the provided client's
// transport that sets the Authorization header with the value produced by the
// provided [cloud.google.com/go/auth.Credentials]. An error is returned only
// if client or creds is nil.
func AddAuthorizationMiddleware(client *http.Client, creds *auth.Credentials) error {
if client == nil || creds == nil {
return fmt.Errorf("httptransport: client and tp must not be nil")
}
base := client.Transport
if base == nil {
base = http.DefaultTransport.(*http.Transport).Clone()
}
client.Transport = &authTransport{
creds: creds,
base: base,
// TODO(quartzmo): Somehow set clientUniverseDomain from impersonate calls.
}
return nil
}
// NewClient returns a [net/http.Client] that can be used to communicate with a
// Google cloud service, configured with the provided [Options]. It
// automatically appends Authorization headers to all outgoing requests.
func NewClient(opts *Options) (*http.Client, error) {
if err := opts.validate(); err != nil {
return nil, err
}
tOpts := &transport.Options{
Endpoint: opts.Endpoint,
ClientCertProvider: opts.ClientCertProvider,
Client: opts.client(),
UniverseDomain: opts.UniverseDomain,
}
if io := opts.InternalOptions; io != nil {
tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
}
clientCertProvider, dialTLSContext, err := transport.GetHTTPTransportConfig(tOpts)
if err != nil {
return nil, err
}
trans, err := newTransport(defaultBaseTransport(clientCertProvider, dialTLSContext), opts)
if err != nil {
return nil, err
}
return &http.Client{
Transport: trans,
}, nil
}
// SetAuthHeader uses the provided token to set the Authorization header on a
// request. If the token.Type is empty, the type is assumed to be Bearer.
func SetAuthHeader(token *auth.Token, req *http.Request) {
typ := token.Type
if typ == "" {
typ = internal.TokenTypeBearer
}
req.Header.Set("Authorization", typ+" "+token.Value)
}

93
vendor/cloud.google.com/go/auth/httptransport/trace.go generated vendored Normal file
View file

@ -0,0 +1,93 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httptransport
import (
"encoding/binary"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"strings"
"go.opencensus.io/trace"
"go.opencensus.io/trace/propagation"
)
const (
httpHeaderMaxSize = 200
cloudTraceHeader = `X-Cloud-Trace-Context`
)
// asserts the httpFormat fulfills this foreign interface
var _ propagation.HTTPFormat = (*httpFormat)(nil)
// httpFormat implements propagation.httpFormat to propagate
// traces in HTTP headers for Google Cloud Platform and Cloud Trace.
type httpFormat struct{}
// SpanContextFromRequest extracts a Cloud Trace span context from incoming requests.
func (f *httpFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) {
h := req.Header.Get(cloudTraceHeader)
// See https://cloud.google.com/trace/docs/faq for the header HTTPFormat.
// Return if the header is empty or missing, or if the header is unreasonably
// large, to avoid making unnecessary copies of a large string.
if h == "" || len(h) > httpHeaderMaxSize {
return trace.SpanContext{}, false
}
// Parse the trace id field.
slash := strings.Index(h, `/`)
if slash == -1 {
return trace.SpanContext{}, false
}
tid, h := h[:slash], h[slash+1:]
buf, err := hex.DecodeString(tid)
if err != nil {
return trace.SpanContext{}, false
}
copy(sc.TraceID[:], buf)
// Parse the span id field.
spanstr := h
semicolon := strings.Index(h, `;`)
if semicolon != -1 {
spanstr, h = h[:semicolon], h[semicolon+1:]
}
sid, err := strconv.ParseUint(spanstr, 10, 64)
if err != nil {
return trace.SpanContext{}, false
}
binary.BigEndian.PutUint64(sc.SpanID[:], sid)
// Parse the options field, options field is optional.
if !strings.HasPrefix(h, "o=") {
return sc, true
}
o, err := strconv.ParseUint(h[2:], 10, 32)
if err != nil {
return trace.SpanContext{}, false
}
sc.TraceOptions = trace.TraceOptions(o)
return sc, true
}
// SpanContextToRequest modifies the given request to include a Cloud Trace header.
func (f *httpFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) {
sid := binary.BigEndian.Uint64(sc.SpanID[:])
header := fmt.Sprintf("%s/%d;o=%d", hex.EncodeToString(sc.TraceID[:]), sid, int64(sc.TraceOptions))
req.Header.Set(cloudTraceHeader, header)
}

View file

@ -0,0 +1,209 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httptransport
import (
"context"
"crypto/tls"
"net"
"net/http"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/transport"
"cloud.google.com/go/auth/internal/transport/cert"
"go.opencensus.io/plugin/ochttp"
"golang.org/x/net/http2"
)
const (
quotaProjectHeaderKey = "X-Goog-User-Project"
)
func newTransport(base http.RoundTripper, opts *Options) (http.RoundTripper, error) {
var headers = opts.Headers
ht := &headerTransport{
base: base,
headers: headers,
}
var trans http.RoundTripper = ht
trans = addOCTransport(trans, opts)
switch {
case opts.DisableAuthentication:
// Do nothing.
case opts.APIKey != "":
qp := internal.GetQuotaProject(nil, opts.Headers.Get(quotaProjectHeaderKey))
if qp != "" {
if headers == nil {
headers = make(map[string][]string, 1)
}
headers.Set(quotaProjectHeaderKey, qp)
}
trans = &apiKeyTransport{
Transport: trans,
Key: opts.APIKey,
}
default:
creds, err := credentials.DetectDefault(opts.resolveDetectOptions())
if err != nil {
return nil, err
}
qp, err := creds.QuotaProjectID(context.Background())
if err != nil {
return nil, err
}
if qp != "" {
if headers == nil {
headers = make(map[string][]string, 1)
}
headers.Set(quotaProjectHeaderKey, qp)
}
if opts.Credentials != nil {
creds = opts.Credentials
}
creds.TokenProvider = auth.NewCachedTokenProvider(creds.TokenProvider, nil)
trans = &authTransport{
base: trans,
creds: creds,
clientUniverseDomain: opts.UniverseDomain,
}
}
return trans, nil
}
// defaultBaseTransport returns the base HTTP transport.
// On App Engine, this is urlfetch.Transport.
// Otherwise, use a default transport, taking most defaults from
// http.DefaultTransport.
// If TLSCertificate is available, set TLSClientConfig as well.
func defaultBaseTransport(clientCertSource cert.Provider, dialTLSContext func(context.Context, string, string) (net.Conn, error)) http.RoundTripper {
trans := http.DefaultTransport.(*http.Transport).Clone()
trans.MaxIdleConnsPerHost = 100
if clientCertSource != nil {
trans.TLSClientConfig = &tls.Config{
GetClientCertificate: clientCertSource,
}
}
if dialTLSContext != nil {
// If DialTLSContext is set, TLSClientConfig wil be ignored
trans.DialTLSContext = dialTLSContext
}
// Configures the ReadIdleTimeout HTTP/2 option for the
// transport. This allows broken idle connections to be pruned more quickly,
// preventing the client from attempting to re-use connections that will no
// longer work.
http2Trans, err := http2.ConfigureTransports(trans)
if err == nil {
http2Trans.ReadIdleTimeout = time.Second * 31
}
return trans
}
type apiKeyTransport struct {
// Key is the API Key to set on requests.
Key string
// Transport is the underlying HTTP transport.
// If nil, http.DefaultTransport is used.
Transport http.RoundTripper
}
func (t *apiKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
newReq := *req
args := newReq.URL.Query()
args.Set("key", t.Key)
newReq.URL.RawQuery = args.Encode()
return t.Transport.RoundTrip(&newReq)
}
type headerTransport struct {
headers http.Header
base http.RoundTripper
}
func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
rt := t.base
newReq := *req
newReq.Header = make(http.Header)
for k, vv := range req.Header {
newReq.Header[k] = vv
}
for k, v := range t.headers {
newReq.Header[k] = v
}
return rt.RoundTrip(&newReq)
}
func addOCTransport(trans http.RoundTripper, opts *Options) http.RoundTripper {
if opts.DisableTelemetry {
return trans
}
return &ochttp.Transport{
Base: trans,
Propagation: &httpFormat{},
}
}
type authTransport struct {
creds *auth.Credentials
base http.RoundTripper
clientUniverseDomain string
}
// getClientUniverseDomain returns the universe domain configured for the client.
// The default value is "googleapis.com".
func (t *authTransport) getClientUniverseDomain() string {
if t.clientUniverseDomain == "" {
return internal.DefaultUniverseDomain
}
return t.clientUniverseDomain
}
// RoundTrip authorizes and authenticates the request with an
// access token from Transport's Source. Per the RoundTripper contract we must
// not modify the initial request, so we clone it, and we must close the body
// on any errors that happens during our token logic.
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
reqBodyClosed := false
if req.Body != nil {
defer func() {
if !reqBodyClosed {
req.Body.Close()
}
}()
}
credentialsUniverseDomain, err := t.creds.UniverseDomain(req.Context())
if err != nil {
return nil, err
}
if err := transport.ValidateUniverseDomain(t.getClientUniverseDomain(), credentialsUniverseDomain); err != nil {
return nil, err
}
token, err := t.creds.Token(req.Context())
if err != nil {
return nil, err
}
req2 := req.Clone(req.Context())
SetAuthHeader(token, req2)
reqBodyClosed = true
return t.base.RoundTrip(req2)
}

View file

@ -0,0 +1,107 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package credsfile is meant to hide implementation details from the pubic
// surface of the detect package. It should not import any other packages in
// this module. It is located under the main internal package so other
// sub-packages can use these parsed types as well.
package credsfile
import (
"os"
"os/user"
"path/filepath"
"runtime"
)
const (
// GoogleAppCredsEnvVar is the environment variable for setting the
// application default credentials.
GoogleAppCredsEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
userCredsFilename = "application_default_credentials.json"
)
// CredentialType represents different credential filetypes Google credentials
// can be.
type CredentialType int
const (
// UnknownCredType is an unidentified file type.
UnknownCredType CredentialType = iota
// UserCredentialsKey represents a user creds file type.
UserCredentialsKey
// ServiceAccountKey represents a service account file type.
ServiceAccountKey
// ImpersonatedServiceAccountKey represents a impersonated service account
// file type.
ImpersonatedServiceAccountKey
// ExternalAccountKey represents a external account file type.
ExternalAccountKey
// GDCHServiceAccountKey represents a GDCH file type.
GDCHServiceAccountKey
// ExternalAccountAuthorizedUserKey represents a external account authorized
// user file type.
ExternalAccountAuthorizedUserKey
)
// parseCredentialType returns the associated filetype based on the parsed
// typeString provided.
func parseCredentialType(typeString string) CredentialType {
switch typeString {
case "service_account":
return ServiceAccountKey
case "authorized_user":
return UserCredentialsKey
case "impersonated_service_account":
return ImpersonatedServiceAccountKey
case "external_account":
return ExternalAccountKey
case "external_account_authorized_user":
return ExternalAccountAuthorizedUserKey
case "gdch_service_account":
return GDCHServiceAccountKey
default:
return UnknownCredType
}
}
// GetFileNameFromEnv returns the override if provided or detects a filename
// from the environment.
func GetFileNameFromEnv(override string) string {
if override != "" {
return override
}
return os.Getenv(GoogleAppCredsEnvVar)
}
// GetWellKnownFileName tries to locate the filepath for the user credential
// file based on the environment.
func GetWellKnownFileName() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("APPDATA"), "gcloud", userCredsFilename)
}
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", userCredsFilename)
}
// guessUnixHomeDir default to checking for HOME, but not all unix systems have
// this set, do have a fallback.
func guessUnixHomeDir() string {
if v := os.Getenv("HOME"); v != "" {
return v
}
if u, err := user.Current(); err == nil {
return u.HomeDir
}
return ""
}

View file

@ -0,0 +1,149 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credsfile
import (
"encoding/json"
)
// Config3LO is the internals of a client creds file.
type Config3LO struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURIs []string `json:"redirect_uris"`
AuthURI string `json:"auth_uri"`
TokenURI string `json:"token_uri"`
}
// ClientCredentialsFile representation.
type ClientCredentialsFile struct {
Web *Config3LO `json:"web"`
Installed *Config3LO `json:"installed"`
UniverseDomain string `json:"universe_domain"`
}
// ServiceAccountFile representation.
type ServiceAccountFile struct {
Type string `json:"type"`
ProjectID string `json:"project_id"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientID string `json:"client_id"`
AuthURL string `json:"auth_uri"`
TokenURL string `json:"token_uri"`
UniverseDomain string `json:"universe_domain"`
}
// UserCredentialsFile representation.
type UserCredentialsFile struct {
Type string `json:"type"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
QuotaProjectID string `json:"quota_project_id"`
RefreshToken string `json:"refresh_token"`
UniverseDomain string `json:"universe_domain"`
}
// ExternalAccountFile representation.
type ExternalAccountFile struct {
Type string `json:"type"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Audience string `json:"audience"`
SubjectTokenType string `json:"subject_token_type"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
TokenURL string `json:"token_url"`
CredentialSource *CredentialSource `json:"credential_source,omitempty"`
TokenInfoURL string `json:"token_info_url"`
ServiceAccountImpersonation *ServiceAccountImpersonationInfo `json:"service_account_impersonation,omitempty"`
QuotaProjectID string `json:"quota_project_id"`
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
UniverseDomain string `json:"universe_domain"`
}
// ExternalAccountAuthorizedUserFile representation.
type ExternalAccountAuthorizedUserFile struct {
Type string `json:"type"`
Audience string `json:"audience"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RefreshToken string `json:"refresh_token"`
TokenURL string `json:"token_url"`
TokenInfoURL string `json:"token_info_url"`
RevokeURL string `json:"revoke_url"`
QuotaProjectID string `json:"quota_project_id"`
UniverseDomain string `json:"universe_domain"`
}
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
//
// One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question.
// The EnvironmentID should start with AWS if being used for an AWS credential.
type CredentialSource struct {
File string `json:"file"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Executable *ExecutableConfig `json:"executable,omitempty"`
EnvironmentID string `json:"environment_id"`
RegionURL string `json:"region_url"`
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
CredVerificationURL string `json:"cred_verification_url"`
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
Format *Format `json:"format,omitempty"`
}
// Format describes the format of a [CredentialSource].
type Format struct {
// Type is either "text" or "json". When not provided "text" type is assumed.
Type string `json:"type"`
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
SubjectTokenFieldName string `json:"subject_token_field_name"`
}
// ExecutableConfig represents the command to run for an executable
// [CredentialSource].
type ExecutableConfig struct {
Command string `json:"command"`
TimeoutMillis int `json:"timeout_millis"`
OutputFile string `json:"output_file"`
}
// ServiceAccountImpersonationInfo has impersonation configuration.
type ServiceAccountImpersonationInfo struct {
TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
}
// ImpersonatedServiceAccountFile representation.
type ImpersonatedServiceAccountFile struct {
Type string `json:"type"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
Delegates []string `json:"delegates"`
CredSource json.RawMessage `json:"source_credentials"`
UniverseDomain string `json:"universe_domain"`
}
// GDCHServiceAccountFile represents the Google Distributed Cloud Hosted (GDCH) service identity file.
type GDCHServiceAccountFile struct {
Type string `json:"type"`
FormatVersion string `json:"format_version"`
Project string `json:"project"`
Name string `json:"name"`
CertPath string `json:"ca_cert_path"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
TokenURL string `json:"token_uri"`
UniverseDomain string `json:"universe_domain"`
}

View file

@ -0,0 +1,98 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package credsfile
import (
"encoding/json"
)
// ParseServiceAccount parses bytes into a [ServiceAccountFile].
func ParseServiceAccount(b []byte) (*ServiceAccountFile, error) {
var f *ServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseClientCredentials parses bytes into a
// [credsfile.ClientCredentialsFile].
func ParseClientCredentials(b []byte) (*ClientCredentialsFile, error) {
var f *ClientCredentialsFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseUserCredentials parses bytes into a [UserCredentialsFile].
func ParseUserCredentials(b []byte) (*UserCredentialsFile, error) {
var f *UserCredentialsFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseExternalAccount parses bytes into a [ExternalAccountFile].
func ParseExternalAccount(b []byte) (*ExternalAccountFile, error) {
var f *ExternalAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseExternalAccountAuthorizedUser parses bytes into a
// [ExternalAccountAuthorizedUserFile].
func ParseExternalAccountAuthorizedUser(b []byte) (*ExternalAccountAuthorizedUserFile, error) {
var f *ExternalAccountAuthorizedUserFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseImpersonatedServiceAccount parses bytes into a
// [ImpersonatedServiceAccountFile].
func ParseImpersonatedServiceAccount(b []byte) (*ImpersonatedServiceAccountFile, error) {
var f *ImpersonatedServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseGDCHServiceAccount parses bytes into a [GDCHServiceAccountFile].
func ParseGDCHServiceAccount(b []byte) (*GDCHServiceAccountFile, error) {
var f *GDCHServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
type fileTypeChecker struct {
Type string `json:"type"`
}
// ParseFileType determines the [CredentialType] based on bytes provided.
func ParseFileType(b []byte) (CredentialType, error) {
var f fileTypeChecker
if err := json.Unmarshal(b, &f); err != nil {
return 0, err
}
return parseCredentialType(f.Type), nil
}

184
vendor/cloud.google.com/go/auth/internal/internal.go generated vendored Normal file
View file

@ -0,0 +1,184 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"os"
"sync"
"time"
"cloud.google.com/go/compute/metadata"
)
const (
// TokenTypeBearer is the auth header prefix for bearer tokens.
TokenTypeBearer = "Bearer"
// QuotaProjectEnvVar is the environment variable for setting the quota
// project.
QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
projectEnvVar = "GOOGLE_CLOUD_PROJECT"
maxBodySize = 1 << 20
// DefaultUniverseDomain is the default value for universe domain.
// Universe domain is the default service domain for a given Cloud universe.
DefaultUniverseDomain = "googleapis.com"
)
// CloneDefaultClient returns a [http.Client] with some good defaults.
func CloneDefaultClient() *http.Client {
return &http.Client{
Transport: http.DefaultTransport.(*http.Transport).Clone(),
Timeout: 30 * time.Second,
}
}
// ParseKey converts the binary contents of a private key file
// to an *rsa.PrivateKey. It detects whether the private key is in a
// PEM container or not. If so, it extracts the the private key
// from PEM container before conversion. It only supports PEM
// containers with no passphrase.
func ParseKey(key []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(key)
if block != nil {
key = block.Bytes
}
parsedKey, err := x509.ParsePKCS8PrivateKey(key)
if err != nil {
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
if err != nil {
return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8: %w", err)
}
}
parsed, ok := parsedKey.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("private key is invalid")
}
return parsed, nil
}
// GetQuotaProject retrieves quota project with precedence being: override,
// environment variable, creds json file.
func GetQuotaProject(b []byte, override string) string {
if override != "" {
return override
}
if env := os.Getenv(QuotaProjectEnvVar); env != "" {
return env
}
if b == nil {
return ""
}
var v struct {
QuotaProject string `json:"quota_project_id"`
}
if err := json.Unmarshal(b, &v); err != nil {
return ""
}
return v.QuotaProject
}
// GetProjectID retrieves project with precedence being: override,
// environment variable, creds json file.
func GetProjectID(b []byte, override string) string {
if override != "" {
return override
}
if env := os.Getenv(projectEnvVar); env != "" {
return env
}
if b == nil {
return ""
}
var v struct {
ProjectID string `json:"project_id"` // standard service account key
Project string `json:"project"` // gdch key
}
if err := json.Unmarshal(b, &v); err != nil {
return ""
}
if v.ProjectID != "" {
return v.ProjectID
}
return v.Project
}
// ReadAll consumes the whole reader and safely reads the content of its body
// with some overflow protection.
func ReadAll(r io.Reader) ([]byte, error) {
return io.ReadAll(io.LimitReader(r, maxBodySize))
}
// StaticCredentialsProperty is a helper for creating static credentials
// properties.
func StaticCredentialsProperty(s string) StaticProperty {
return StaticProperty(s)
}
// StaticProperty always returns that value of the underlying string.
type StaticProperty string
// GetProperty loads the properly value provided the given context.
func (p StaticProperty) GetProperty(context.Context) (string, error) {
return string(p), nil
}
// ComputeUniverseDomainProvider fetches the credentials universe domain from
// the google cloud metadata service.
type ComputeUniverseDomainProvider struct {
universeDomainOnce sync.Once
universeDomain string
universeDomainErr error
}
// GetProperty fetches the credentials universe domain from the google cloud
// metadata service.
func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) {
c.universeDomainOnce.Do(func() {
c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx)
})
if c.universeDomainErr != nil {
return "", c.universeDomainErr
}
return c.universeDomain, nil
}
// httpGetMetadataUniverseDomain is a package var for unit test substitution.
var httpGetMetadataUniverseDomain = func(ctx context.Context) (string, error) {
client := metadata.NewClient(&http.Client{Timeout: time.Second})
// TODO(quartzmo): set ctx on request
return client.Get("universe/universe_domain")
}
func getMetadataUniverseDomain(ctx context.Context) (string, error) {
universeDomain, err := httpGetMetadataUniverseDomain(ctx)
if err == nil {
return universeDomain, nil
}
if _, ok := err.(metadata.NotDefinedError); ok {
// http.StatusNotFound (404)
return DefaultUniverseDomain, nil
}
return "", err
}

171
vendor/cloud.google.com/go/auth/internal/jwt/jwt.go generated vendored Normal file
View file

@ -0,0 +1,171 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jwt
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
)
const (
// HeaderAlgRSA256 is the RS256 [Header.Algorithm].
HeaderAlgRSA256 = "RS256"
// HeaderAlgES256 is the ES256 [Header.Algorithm].
HeaderAlgES256 = "ES256"
// HeaderType is the standard [Header.Type].
HeaderType = "JWT"
)
// Header represents a JWT header.
type Header struct {
Algorithm string `json:"alg"`
Type string `json:"typ"`
KeyID string `json:"kid"`
}
func (h *Header) encode() (string, error) {
b, err := json.Marshal(h)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// Claims represents the claims set of a JWT.
type Claims struct {
// Iss is the issuer JWT claim.
Iss string `json:"iss"`
// Scope is the scope JWT claim.
Scope string `json:"scope,omitempty"`
// Exp is the expiry JWT claim. If unset, default is in one hour from now.
Exp int64 `json:"exp"`
// Iat is the subject issued at claim. If unset, default is now.
Iat int64 `json:"iat"`
// Aud is the audience JWT claim. Optional.
Aud string `json:"aud"`
// Sub is the subject JWT claim. Optional.
Sub string `json:"sub,omitempty"`
// AdditionalClaims contains any additional non-standard JWT claims. Optional.
AdditionalClaims map[string]interface{} `json:"-"`
}
func (c *Claims) encode() (string, error) {
// Compensate for skew
now := time.Now().Add(-10 * time.Second)
if c.Iat == 0 {
c.Iat = now.Unix()
}
if c.Exp == 0 {
c.Exp = now.Add(time.Hour).Unix()
}
if c.Exp < c.Iat {
return "", fmt.Errorf("jwt: invalid Exp = %d; must be later than Iat = %d", c.Exp, c.Iat)
}
b, err := json.Marshal(c)
if err != nil {
return "", err
}
if len(c.AdditionalClaims) == 0 {
return base64.RawURLEncoding.EncodeToString(b), nil
}
// Marshal private claim set and then append it to b.
prv, err := json.Marshal(c.AdditionalClaims)
if err != nil {
return "", fmt.Errorf("invalid map of additional claims %v: %w", c.AdditionalClaims, err)
}
// Concatenate public and private claim JSON objects.
if !bytes.HasSuffix(b, []byte{'}'}) {
return "", fmt.Errorf("invalid JSON %s", b)
}
if !bytes.HasPrefix(prv, []byte{'{'}) {
return "", fmt.Errorf("invalid JSON %s", prv)
}
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
b = append(b, prv[1:]...) // Append private claims.
return base64.RawURLEncoding.EncodeToString(b), nil
}
// EncodeJWS encodes the data using the provided key as a JSON web signature.
func EncodeJWS(header *Header, c *Claims, key *rsa.PrivateKey) (string, error) {
head, err := header.encode()
if err != nil {
return "", err
}
claims, err := c.encode()
if err != nil {
return "", err
}
ss := fmt.Sprintf("%s.%s", head, claims)
h := sha256.New()
h.Write([]byte(ss))
sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h.Sum(nil))
if err != nil {
return "", err
}
return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil
}
// DecodeJWS decodes a claim set from a JWS payload.
func DecodeJWS(payload string) (*Claims, error) {
// decode returned id token to get expiry
s := strings.Split(payload, ".")
if len(s) < 2 {
return nil, errors.New("invalid token received")
}
decoded, err := base64.RawURLEncoding.DecodeString(s[1])
if err != nil {
return nil, err
}
c := &Claims{}
if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c); err != nil {
return nil, err
}
if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(&c.AdditionalClaims); err != nil {
return nil, err
}
return c, err
}
// VerifyJWS tests whether the provided JWT token's signature was produced by
// the private key associated with the provided public key.
func VerifyJWS(token string, key *rsa.PublicKey) error {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return errors.New("jwt: invalid token received, token must have 3 parts")
}
signedContent := parts[0] + "." + parts[1]
signatureString, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return err
}
h := sha256.New()
h.Write([]byte(signedContent))
return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString)
}

View file

@ -0,0 +1,300 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/transport/cert"
"github.com/google/s2a-go"
"github.com/google/s2a-go/fallback"
"google.golang.org/grpc/credentials"
)
const (
mTLSModeAlways = "always"
mTLSModeNever = "never"
mTLSModeAuto = "auto"
// Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false.
googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A"
googleAPIUseCertSource = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
googleAPIUseMTLS = "GOOGLE_API_USE_MTLS_ENDPOINT"
googleAPIUseMTLSOld = "GOOGLE_API_USE_MTLS"
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
)
var (
mdsMTLSAutoConfigSource mtlsConfigSource
errUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com")
)
// Options is a struct that is duplicated information from the individual
// transport packages in order to avoid cyclic deps. It correlates 1:1 with
// fields on httptransport.Options and grpctransport.Options.
type Options struct {
Endpoint string
DefaultMTLSEndpoint string
DefaultEndpointTemplate string
ClientCertProvider cert.Provider
Client *http.Client
UniverseDomain string
}
// getUniverseDomain returns the default service domain for a given Cloud
// universe.
func (o *Options) getUniverseDomain() string {
if o.UniverseDomain == "" {
return internal.DefaultUniverseDomain
}
return o.UniverseDomain
}
// isUniverseDomainGDU returns true if the universe domain is the default Google
// universe.
func (o *Options) isUniverseDomainGDU() bool {
return o.getUniverseDomain() == internal.DefaultUniverseDomain
}
// defaultEndpoint returns the DefaultEndpointTemplate merged with the
// universe domain if the DefaultEndpointTemplate is set, otherwise returns an
// empty string.
func (o *Options) defaultEndpoint() string {
if o.DefaultEndpointTemplate == "" {
return ""
}
return strings.Replace(o.DefaultEndpointTemplate, universeDomainPlaceholder, o.getUniverseDomain(), 1)
}
// mergedEndpoint merges a user-provided Endpoint of format host[:port] with the
// default endpoint.
func (o *Options) mergedEndpoint() (string, error) {
defaultEndpoint := o.defaultEndpoint()
u, err := url.Parse(fixScheme(defaultEndpoint))
if err != nil {
return "", err
}
return strings.Replace(defaultEndpoint, u.Host, o.Endpoint, 1), nil
}
func fixScheme(baseURL string) string {
if !strings.Contains(baseURL, "://") {
baseURL = "https://" + baseURL
}
return baseURL
}
// GetGRPCTransportCredsAndEndpoint returns an instance of
// [google.golang.org/grpc/credentials.TransportCredentials], and the
// corresponding endpoint to use for GRPC client.
func GetGRPCTransportCredsAndEndpoint(opts *Options) (credentials.TransportCredentials, string, error) {
config, err := getTransportConfig(opts)
if err != nil {
return nil, "", err
}
defaultTransportCreds := credentials.NewTLS(&tls.Config{
GetClientCertificate: config.clientCertSource,
})
if config.s2aAddress == "" {
return defaultTransportCreds, config.endpoint, nil
}
var fallbackOpts *s2a.FallbackOptions
// In case of S2A failure, fall back to the endpoint that would've been used without S2A.
if fallbackHandshake, err := fallback.DefaultFallbackClientHandshakeFunc(config.endpoint); err == nil {
fallbackOpts = &s2a.FallbackOptions{
FallbackClientHandshakeFunc: fallbackHandshake,
}
}
s2aTransportCreds, err := s2a.NewClientCreds(&s2a.ClientOptions{
S2AAddress: config.s2aAddress,
FallbackOpts: fallbackOpts,
})
if err != nil {
// Use default if we cannot initialize S2A client transport credentials.
return defaultTransportCreds, config.endpoint, nil
}
return s2aTransportCreds, config.s2aMTLSEndpoint, nil
}
// GetHTTPTransportConfig returns a client certificate source and a function for
// dialing MTLS with S2A.
func GetHTTPTransportConfig(opts *Options) (cert.Provider, func(context.Context, string, string) (net.Conn, error), error) {
config, err := getTransportConfig(opts)
if err != nil {
return nil, nil, err
}
if config.s2aAddress == "" {
return config.clientCertSource, nil, nil
}
var fallbackOpts *s2a.FallbackOptions
// In case of S2A failure, fall back to the endpoint that would've been used without S2A.
if fallbackURL, err := url.Parse(config.endpoint); err == nil {
if fallbackDialer, fallbackServerAddr, err := fallback.DefaultFallbackDialerAndAddress(fallbackURL.Hostname()); err == nil {
fallbackOpts = &s2a.FallbackOptions{
FallbackDialer: &s2a.FallbackDialer{
Dialer: fallbackDialer,
ServerAddr: fallbackServerAddr,
},
}
}
}
dialTLSContextFunc := s2a.NewS2ADialTLSContextFunc(&s2a.ClientOptions{
S2AAddress: config.s2aAddress,
FallbackOpts: fallbackOpts,
})
return nil, dialTLSContextFunc, nil
}
func getTransportConfig(opts *Options) (*transportConfig, error) {
clientCertSource, err := getClientCertificateSource(opts)
if err != nil {
return nil, err
}
endpoint, err := getEndpoint(opts, clientCertSource)
if err != nil {
return nil, err
}
defaultTransportConfig := transportConfig{
clientCertSource: clientCertSource,
endpoint: endpoint,
}
if !shouldUseS2A(clientCertSource, opts) {
return &defaultTransportConfig, nil
}
if !opts.isUniverseDomainGDU() {
return nil, errUniverseNotSupportedMTLS
}
s2aMTLSEndpoint := opts.DefaultMTLSEndpoint
// If there is endpoint override, honor it.
if opts.Endpoint != "" {
s2aMTLSEndpoint = endpoint
}
s2aAddress := GetS2AAddress()
if s2aAddress == "" {
return &defaultTransportConfig, nil
}
return &transportConfig{
clientCertSource: clientCertSource,
endpoint: endpoint,
s2aAddress: s2aAddress,
s2aMTLSEndpoint: s2aMTLSEndpoint,
}, nil
}
// getClientCertificateSource returns a default client certificate source, if
// not provided by the user.
//
// A nil default source can be returned if the source does not exist. Any exceptions
// encountered while initializing the default source will be reported as client
// error (ex. corrupt metadata file).
//
// Important Note: For now, the environment variable GOOGLE_API_USE_CLIENT_CERTIFICATE
// must be set to "true" to allow certificate to be used (including user provided
// certificates). For details, see AIP-4114.
func getClientCertificateSource(opts *Options) (cert.Provider, error) {
if !isClientCertificateEnabled() {
return nil, nil
} else if opts.ClientCertProvider != nil {
return opts.ClientCertProvider, nil
}
return cert.DefaultProvider()
}
func isClientCertificateEnabled() bool {
// TODO(andyrzhao): Update default to return "true" after DCA feature is fully released.
// error as false is a good default
b, _ := strconv.ParseBool(os.Getenv(googleAPIUseCertSource))
return b
}
type transportConfig struct {
// The client certificate source.
clientCertSource cert.Provider
// The corresponding endpoint to use based on client certificate source.
endpoint string
// The S2A address if it can be used, otherwise an empty string.
s2aAddress string
// The MTLS endpoint to use with S2A.
s2aMTLSEndpoint string
}
// getEndpoint returns the endpoint for the service, taking into account the
// user-provided endpoint override "settings.Endpoint".
//
// If no endpoint override is specified, we will either return the default endpoint or
// the default mTLS endpoint if a client certificate is available.
//
// You can override the default endpoint choice (mtls vs. regular) by setting the
// GOOGLE_API_USE_MTLS_ENDPOINT environment variable.
//
// If the endpoint override is an address (host:port) rather than full base
// URL (ex. https://...), then the user-provided address will be merged into
// the default endpoint. For example, WithEndpoint("myhost:8000") and
// DefaultEndpointTemplate("https://UNIVERSE_DOMAIN/bar/baz") will return "https://myhost:8080/bar/baz"
func getEndpoint(opts *Options, clientCertSource cert.Provider) (string, error) {
if opts.Endpoint == "" {
mtlsMode := getMTLSMode()
if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
if !opts.isUniverseDomainGDU() {
return "", errUniverseNotSupportedMTLS
}
return opts.DefaultMTLSEndpoint, nil
}
return opts.defaultEndpoint(), nil
}
if strings.Contains(opts.Endpoint, "://") {
// User passed in a full URL path, use it verbatim.
return opts.Endpoint, nil
}
if opts.defaultEndpoint() == "" {
// If DefaultEndpointTemplate is not configured,
// use the user provided endpoint verbatim. This allows a naked
// "host[:port]" URL to be used with GRPC Direct Path.
return opts.Endpoint, nil
}
// Assume user-provided endpoint is host[:port], merge it with the default endpoint.
return opts.mergedEndpoint()
}
func getMTLSMode() string {
mode := os.Getenv(googleAPIUseMTLS)
if mode == "" {
mode = os.Getenv(googleAPIUseMTLSOld) // Deprecated.
}
if mode == "" {
return mTLSModeAuto
}
return strings.ToLower(mode)
}

View file

@ -0,0 +1,62 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cert
import (
"crypto/tls"
"errors"
"sync"
)
// defaultCertData holds all the variables pertaining to
// the default certificate provider created by [DefaultProvider].
//
// A singleton model is used to allow the provider to be reused
// by the transport layer. As mentioned in [DefaultProvider] (provider nil, nil)
// may be returned to indicate a default provider could not be found, which
// will skip extra tls config in the transport layer .
type defaultCertData struct {
once sync.Once
provider Provider
err error
}
var (
defaultCert defaultCertData
)
// Provider is a function that can be passed into crypto/tls.Config.GetClientCertificate.
type Provider func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
// errSourceUnavailable is a sentinel error to indicate certificate source is unavailable.
var errSourceUnavailable = errors.New("certificate source is unavailable")
// DefaultProvider returns a certificate source using the preferred EnterpriseCertificateProxySource.
// If EnterpriseCertificateProxySource is not available, fall back to the legacy SecureConnectSource.
//
// If neither source is available (due to missing configurations), a nil Source and a nil Error are
// returned to indicate that a default certificate source is unavailable.
func DefaultProvider() (Provider, error) {
defaultCert.once.Do(func() {
defaultCert.provider, defaultCert.err = NewEnterpriseCertificateProxyProvider("")
if errors.Is(defaultCert.err, errSourceUnavailable) {
defaultCert.provider, defaultCert.err = NewSecureConnectProvider("")
if errors.Is(defaultCert.err, errSourceUnavailable) {
defaultCert.provider, defaultCert.err = nil, nil
}
}
})
return defaultCert.provider, defaultCert.err
}

View file

@ -0,0 +1,56 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cert
import (
"crypto/tls"
"errors"
"github.com/googleapis/enterprise-certificate-proxy/client"
)
type ecpSource struct {
key *client.Key
}
// NewEnterpriseCertificateProxyProvider creates a certificate source
// using the Enterprise Certificate Proxy client, which delegates
// certifcate related operations to an OS-specific "signer binary"
// that communicates with the native keystore (ex. keychain on MacOS).
//
// The configFilePath points to a config file containing relevant parameters
// such as the certificate issuer and the location of the signer binary.
// If configFilePath is empty, the client will attempt to load the config from
// a well-known gcloud location.
func NewEnterpriseCertificateProxyProvider(configFilePath string) (Provider, error) {
key, err := client.Cred(configFilePath)
if err != nil {
if errors.Is(err, client.ErrCredUnavailable) {
return nil, errSourceUnavailable
}
return nil, err
}
return (&ecpSource{
key: key,
}).getClientCertificate, nil
}
func (s *ecpSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
var cert tls.Certificate
cert.PrivateKey = s.key
cert.Certificate = s.key.CertificateChain()
return &cert, nil
}

View file

@ -0,0 +1,124 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cert
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"sync"
"time"
)
const (
metadataPath = ".secureConnect"
metadataFile = "context_aware_metadata.json"
)
type secureConnectSource struct {
metadata secureConnectMetadata
// Cache the cert to avoid executing helper command repeatedly.
cachedCertMutex sync.Mutex
cachedCert *tls.Certificate
}
type secureConnectMetadata struct {
Cmd []string `json:"cert_provider_command"`
}
// NewSecureConnectProvider creates a certificate source using
// the Secure Connect Helper and its associated metadata file.
//
// The configFilePath points to the location of the context aware metadata file.
// If configFilePath is empty, use the default context aware metadata location.
func NewSecureConnectProvider(configFilePath string) (Provider, error) {
if configFilePath == "" {
user, err := user.Current()
if err != nil {
// Error locating the default config means Secure Connect is not supported.
return nil, errSourceUnavailable
}
configFilePath = filepath.Join(user.HomeDir, metadataPath, metadataFile)
}
file, err := os.ReadFile(configFilePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Config file missing means Secure Connect is not supported.
return nil, errSourceUnavailable
}
return nil, err
}
var metadata secureConnectMetadata
if err := json.Unmarshal(file, &metadata); err != nil {
return nil, fmt.Errorf("cert: could not parse JSON in %q: %w", configFilePath, err)
}
if err := validateMetadata(metadata); err != nil {
return nil, fmt.Errorf("cert: invalid config in %q: %w", configFilePath, err)
}
return (&secureConnectSource{
metadata: metadata,
}).getClientCertificate, nil
}
func validateMetadata(metadata secureConnectMetadata) error {
if len(metadata.Cmd) == 0 {
return errors.New("empty cert_provider_command")
}
return nil
}
func (s *secureConnectSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
s.cachedCertMutex.Lock()
defer s.cachedCertMutex.Unlock()
if s.cachedCert != nil && !isCertificateExpired(s.cachedCert) {
return s.cachedCert, nil
}
// Expand OS environment variables in the cert provider command such as "$HOME".
for i := 0; i < len(s.metadata.Cmd); i++ {
s.metadata.Cmd[i] = os.ExpandEnv(s.metadata.Cmd[i])
}
command := s.metadata.Cmd
data, err := exec.Command(command[0], command[1:]...).Output()
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(data, data)
if err != nil {
return nil, err
}
s.cachedCert = &cert
return &cert, nil
}
// isCertificateExpired returns true if the given cert is expired or invalid.
func isCertificateExpired(cert *tls.Certificate) bool {
if len(cert.Certificate) == 0 {
return true
}
parsed, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return true
}
return time.Now().After(parsed.NotAfter)
}

View file

@ -0,0 +1,189 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import (
"encoding/json"
"log"
"os"
"strconv"
"sync"
"time"
"cloud.google.com/go/auth/internal/transport/cert"
"cloud.google.com/go/compute/metadata"
)
const (
configEndpointSuffix = "instance/platform-security/auto-mtls-configuration"
)
var (
// The period an MTLS config can be reused before needing refresh.
configExpiry = time.Hour
// mtlsEndpointEnabledForS2A checks if the endpoint is indeed MTLS-enabled, so that we can use S2A for MTLS connection.
mtlsEndpointEnabledForS2A = func() bool {
// TODO(xmenxk): determine this via discovery config.
return true
}
// mdsMTLSAutoConfigSource is an instance of reuseMTLSConfigSource, with metadataMTLSAutoConfig as its config source.
mtlsOnce sync.Once
)
// GetS2AAddress returns the S2A address to be reached via plaintext connection.
// Returns empty string if not set or invalid.
func GetS2AAddress() string {
c, err := getMetadataMTLSAutoConfig().Config()
if err != nil {
return ""
}
if !c.Valid() {
return ""
}
return c.S2A.PlaintextAddress
}
type mtlsConfigSource interface {
Config() (*mtlsConfig, error)
}
// mtlsConfig contains the configuration for establishing MTLS connections with Google APIs.
type mtlsConfig struct {
S2A *s2aAddresses `json:"s2a"`
Expiry time.Time
}
func (c *mtlsConfig) Valid() bool {
return c != nil && c.S2A != nil && !c.expired()
}
func (c *mtlsConfig) expired() bool {
return c.Expiry.Before(time.Now())
}
// s2aAddresses contains the plaintext and/or MTLS S2A addresses.
type s2aAddresses struct {
// PlaintextAddress is the plaintext address to reach S2A
PlaintextAddress string `json:"plaintext_address"`
// MTLSAddress is the MTLS address to reach S2A
MTLSAddress string `json:"mtls_address"`
}
// getMetadataMTLSAutoConfig returns mdsMTLSAutoConfigSource, which is backed by config from MDS with auto-refresh.
func getMetadataMTLSAutoConfig() mtlsConfigSource {
mtlsOnce.Do(func() {
mdsMTLSAutoConfigSource = &reuseMTLSConfigSource{
src: &metadataMTLSAutoConfig{},
}
})
return mdsMTLSAutoConfigSource
}
// reuseMTLSConfigSource caches a valid version of mtlsConfig, and uses `src` to refresh upon config expiry.
// It implements the mtlsConfigSource interface, so calling Config() on it returns an mtlsConfig.
type reuseMTLSConfigSource struct {
src mtlsConfigSource // src.Config() is called when config is expired
mu sync.Mutex // mutex guards config
config *mtlsConfig // cached config
}
func (cs *reuseMTLSConfigSource) Config() (*mtlsConfig, error) {
cs.mu.Lock()
defer cs.mu.Unlock()
if cs.config.Valid() {
return cs.config, nil
}
c, err := cs.src.Config()
if err != nil {
return nil, err
}
cs.config = c
return c, nil
}
// metadataMTLSAutoConfig is an implementation of the interface mtlsConfigSource
// It has the logic to query MDS and return an mtlsConfig
type metadataMTLSAutoConfig struct{}
var httpGetMetadataMTLSConfig = func() (string, error) {
return metadata.Get(configEndpointSuffix)
}
func (cs *metadataMTLSAutoConfig) Config() (*mtlsConfig, error) {
resp, err := httpGetMetadataMTLSConfig()
if err != nil {
log.Printf("querying MTLS config from MDS endpoint failed: %v", err)
return defaultMTLSConfig(), nil
}
var config mtlsConfig
err = json.Unmarshal([]byte(resp), &config)
if err != nil {
log.Printf("unmarshalling MTLS config from MDS endpoint failed: %v", err)
return defaultMTLSConfig(), nil
}
if config.S2A == nil {
log.Printf("returned MTLS config from MDS endpoint is invalid: %v", config)
return defaultMTLSConfig(), nil
}
// set new expiry
config.Expiry = time.Now().Add(configExpiry)
return &config, nil
}
func defaultMTLSConfig() *mtlsConfig {
return &mtlsConfig{
S2A: &s2aAddresses{
PlaintextAddress: "",
MTLSAddress: "",
},
Expiry: time.Now().Add(configExpiry),
}
}
func shouldUseS2A(clientCertSource cert.Provider, opts *Options) bool {
// If client cert is found, use that over S2A.
if clientCertSource != nil {
return false
}
// If EXPERIMENTAL_GOOGLE_API_USE_S2A is not set to true, skip S2A.
if !isGoogleS2AEnabled() {
return false
}
// If DefaultMTLSEndpoint is not set and no endpoint override, skip S2A.
if opts.DefaultMTLSEndpoint == "" && opts.Endpoint == "" {
return false
}
// If MTLS is not enabled for this endpoint, skip S2A.
if !mtlsEndpointEnabledForS2A() {
return false
}
// If custom HTTP client is provided, skip S2A.
if opts.Client != nil {
return false
}
return true
}
func isGoogleS2AEnabled() bool {
b, err := strconv.ParseBool(os.Getenv(googleAPIUseS2AEnv))
if err != nil {
return false
}
return b
}

View file

@ -0,0 +1,76 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package transport provided internal helpers for the two transport packages
// (grpctransport and httptransport).
package transport
import (
"fmt"
"cloud.google.com/go/auth/credentials"
)
// CloneDetectOptions clones a user set detect option into some new memory that
// we can internally manipulate before sending onto the detect package.
func CloneDetectOptions(oldDo *credentials.DetectOptions) *credentials.DetectOptions {
if oldDo == nil {
// it is valid for users not to set this, but we will need to to default
// some options for them in this case so return some initialized memory
// to work with.
return &credentials.DetectOptions{}
}
newDo := &credentials.DetectOptions{
// Simple types
Audience: oldDo.Audience,
Subject: oldDo.Subject,
EarlyTokenRefresh: oldDo.EarlyTokenRefresh,
TokenURL: oldDo.TokenURL,
STSAudience: oldDo.STSAudience,
CredentialsFile: oldDo.CredentialsFile,
UseSelfSignedJWT: oldDo.UseSelfSignedJWT,
UniverseDomain: oldDo.UniverseDomain,
// These fields are are pointer types that we just want to use exactly
// as the user set, copy the ref
Client: oldDo.Client,
AuthHandlerOptions: oldDo.AuthHandlerOptions,
}
// Smartly size this memory and copy below.
if oldDo.CredentialsJSON != nil {
newDo.CredentialsJSON = make([]byte, len(oldDo.CredentialsJSON))
copy(newDo.CredentialsJSON, oldDo.CredentialsJSON)
}
if oldDo.Scopes != nil {
newDo.Scopes = make([]string, len(oldDo.Scopes))
copy(newDo.Scopes, oldDo.Scopes)
}
return newDo
}
// ValidateUniverseDomain verifies that the universe domain configured for the
// client matches the universe domain configured for the credentials.
func ValidateUniverseDomain(clientUniverseDomain, credentialsUniverseDomain string) error {
if clientUniverseDomain != credentialsUniverseDomain {
return fmt.Errorf(
"the configured universe domain (%q) does not match the universe "+
"domain found in the credentials (%q). If you haven't configured "+
"the universe domain explicitly, \"googleapis.com\" is the default",
clientUniverseDomain,
credentialsUniverseDomain)
}
return nil
}

26
vendor/cloud.google.com/go/auth/oauth2adapt/CHANGES.md generated vendored Normal file
View file

@ -0,0 +1,26 @@
# Changelog
## [0.2.0](https://github.com/googleapis/google-cloud-go/compare/auth/oauth2adapt/v0.1.0...auth/oauth2adapt/v0.2.0) (2024-04-16)
### Features
* **auth/oauth2adapt:** Add helpers for working with credentials types ([#9694](https://github.com/googleapis/google-cloud-go/issues/9694)) ([cf33b55](https://github.com/googleapis/google-cloud-go/commit/cf33b5514423a2ac5c2a323a1cd99aac34fd4233))
### Bug Fixes
* **auth/oauth2adapt:** Update protobuf dep to v1.33.0 ([30b038d](https://github.com/googleapis/google-cloud-go/commit/30b038d8cac0b8cd5dd4761c87f3f298760dd33a))
## 0.1.0 (2023-10-19)
### Features
* **auth/oauth2adapt:** Adds a new module to translate types ([#8595](https://github.com/googleapis/google-cloud-go/issues/8595)) ([6933c5a](https://github.com/googleapis/google-cloud-go/commit/6933c5a0c1fc8e58cbfff8bbca439d671b94672f))
* **auth/oauth2adapt:** Fixup deps for release ([#8747](https://github.com/googleapis/google-cloud-go/issues/8747)) ([749d243](https://github.com/googleapis/google-cloud-go/commit/749d243862b025a6487a4d2d339219889b4cfe70))
### Bug Fixes
* **auth/oauth2adapt:** Update golang.org/x/net to v0.17.0 ([174da47](https://github.com/googleapis/google-cloud-go/commit/174da47254fefb12921bbfc65b7829a453af6f5d))

202
vendor/cloud.google.com/go/auth/oauth2adapt/LICENSE generated vendored Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,162 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package oauth2adapt helps converts types used in [cloud.google.com/go/auth]
// and [golang.org/x/oauth2].
package oauth2adapt
import (
"context"
"encoding/json"
"errors"
"cloud.google.com/go/auth"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
// TokenProviderFromTokenSource converts any [golang.org/x/oauth2.TokenSource]
// into a [cloud.google.com/go/auth.TokenProvider].
func TokenProviderFromTokenSource(ts oauth2.TokenSource) auth.TokenProvider {
return &tokenProviderAdapter{ts: ts}
}
type tokenProviderAdapter struct {
ts oauth2.TokenSource
}
// Token fulfills the [cloud.google.com/go/auth.TokenProvider] interface. It
// is a light wrapper around the underlying TokenSource.
func (tp *tokenProviderAdapter) Token(context.Context) (*auth.Token, error) {
tok, err := tp.ts.Token()
if err != nil {
var err2 *oauth2.RetrieveError
if ok := errors.As(err, &err2); ok {
return nil, AuthErrorFromRetrieveError(err2)
}
return nil, err
}
return &auth.Token{
Value: tok.AccessToken,
Expiry: tok.Expiry,
}, nil
}
// TokenSourceFromTokenProvider converts any
// [cloud.google.com/go/auth.TokenProvider] into a
// [golang.org/x/oauth2.TokenSource].
func TokenSourceFromTokenProvider(tp auth.TokenProvider) oauth2.TokenSource {
return &tokenSourceAdapter{tp: tp}
}
type tokenSourceAdapter struct {
tp auth.TokenProvider
}
// Token fulfills the [golang.org/x/oauth2.TokenSource] interface. It
// is a light wrapper around the underlying TokenProvider.
func (ts *tokenSourceAdapter) Token() (*oauth2.Token, error) {
tok, err := ts.tp.Token(context.Background())
if err != nil {
var err2 *auth.Error
if ok := errors.As(err, &err2); ok {
return nil, AddRetrieveErrorToAuthError(err2)
}
return nil, err
}
return &oauth2.Token{
AccessToken: tok.Value,
Expiry: tok.Expiry,
}, nil
}
// AuthCredentialsFromOauth2Credentials converts a [golang.org/x/oauth2/google.Credentials]
// to a [cloud.google.com/go/auth.Credentials].
func AuthCredentialsFromOauth2Credentials(creds *google.Credentials) *auth.Credentials {
if creds == nil {
return nil
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: TokenProviderFromTokenSource(creds.TokenSource),
JSON: creds.JSON,
ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
return creds.ProjectID, nil
}),
UniverseDomainProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
return creds.GetUniverseDomain()
}),
})
}
// Oauth2CredentialsFromAuthCredentials converts a [cloud.google.com/go/auth.Credentials]
// to a [golang.org/x/oauth2/google.Credentials].
func Oauth2CredentialsFromAuthCredentials(creds *auth.Credentials) *google.Credentials {
if creds == nil {
return nil
}
// Throw away errors as old credentials are not request aware. Also, no
// network requests are currently happening for this use case.
projectID, _ := creds.ProjectID(context.Background())
return &google.Credentials{
TokenSource: TokenSourceFromTokenProvider(creds.TokenProvider),
ProjectID: projectID,
JSON: creds.JSON(),
UniverseDomainProvider: func() (string, error) {
return creds.UniverseDomain(context.Background())
},
}
}
type oauth2Error struct {
ErrorCode string `json:"error"`
ErrorDescription string `json:"error_description"`
ErrorURI string `json:"error_uri"`
}
// AddRetrieveErrorToAuthError returns the same error provided and adds a
// [golang.org/x/oauth2.RetrieveError] to the error chain by setting the `Err` field on the
// [cloud.google.com/go/auth.Error].
func AddRetrieveErrorToAuthError(err *auth.Error) *auth.Error {
if err == nil {
return nil
}
e := &oauth2.RetrieveError{
Response: err.Response,
Body: err.Body,
}
err.Err = e
if len(err.Body) > 0 {
var oErr oauth2Error
// ignore the error as it only fills in extra details
json.Unmarshal(err.Body, &oErr)
e.ErrorCode = oErr.ErrorCode
e.ErrorDescription = oErr.ErrorDescription
e.ErrorURI = oErr.ErrorURI
}
return err
}
// AuthErrorFromRetrieveError returns an [cloud.google.com/go/auth.Error] that
// wraps the provided [golang.org/x/oauth2.RetrieveError].
func AuthErrorFromRetrieveError(err *oauth2.RetrieveError) *auth.Error {
if err == nil {
return nil
}
return &auth.Error{
Response: err.Response,
Body: err.Body,
Err: err,
}
}

373
vendor/cloud.google.com/go/auth/threelegged.go generated vendored Normal file
View file

@ -0,0 +1,373 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"mime"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"cloud.google.com/go/auth/internal"
)
// AuthorizationHandler is a 3-legged-OAuth helper that prompts the user for
// OAuth consent at the specified auth code URL and returns an auth code and
// state upon approval.
type AuthorizationHandler func(authCodeURL string) (code string, state string, err error)
// Options3LO are the options for doing a 3-legged OAuth2 flow.
type Options3LO struct {
// ClientID is the application's ID.
ClientID string
// ClientSecret is the application's secret. Not required if AuthHandlerOpts
// is set.
ClientSecret string
// AuthURL is the URL for authenticating.
AuthURL string
// TokenURL is the URL for retrieving a token.
TokenURL string
// AuthStyle is used to describe how to client info in the token request.
AuthStyle Style
// RefreshToken is the token used to refresh the credential. Not required
// if AuthHandlerOpts is set.
RefreshToken string
// RedirectURL is the URL to redirect users to. Optional.
RedirectURL string
// Scopes specifies requested permissions for the Token. Optional.
Scopes []string
// URLParams are the set of values to apply to the token exchange. Optional.
URLParams url.Values
// Client is the client to be used to make the underlying token requests.
// Optional.
Client *http.Client
// EarlyTokenExpiry is the time before the token expires that it should be
// refreshed. If not set the default value is 10 seconds. Optional.
EarlyTokenExpiry time.Duration
// AuthHandlerOpts provides a set of options for doing a
// 3-legged OAuth2 flow with a custom [AuthorizationHandler]. Optional.
AuthHandlerOpts *AuthorizationHandlerOptions
}
func (o *Options3LO) validate() error {
if o == nil {
return errors.New("auth: options must be provided")
}
if o.ClientID == "" {
return errors.New("auth: client ID must be provided")
}
if o.AuthHandlerOpts == nil && o.ClientSecret == "" {
return errors.New("auth: client secret must be provided")
}
if o.AuthURL == "" {
return errors.New("auth: auth URL must be provided")
}
if o.TokenURL == "" {
return errors.New("auth: token URL must be provided")
}
if o.AuthStyle == StyleUnknown {
return errors.New("auth: auth style must be provided")
}
if o.AuthHandlerOpts == nil && o.RefreshToken == "" {
return errors.New("auth: refresh token must be provided")
}
return nil
}
// PKCEOptions holds parameters to support PKCE.
type PKCEOptions struct {
// Challenge is the un-padded, base64-url-encoded string of the encrypted code verifier.
Challenge string // The un-padded, base64-url-encoded string of the encrypted code verifier.
// ChallengeMethod is the encryption method (ex. S256).
ChallengeMethod string
// Verifier is the original, non-encrypted secret.
Verifier string // The original, non-encrypted secret.
}
type tokenJSON struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
// error fields
ErrorCode string `json:"error"`
ErrorDescription string `json:"error_description"`
ErrorURI string `json:"error_uri"`
}
func (e *tokenJSON) expiry() (t time.Time) {
if v := e.ExpiresIn; v != 0 {
return time.Now().Add(time.Duration(v) * time.Second)
}
return
}
func (o *Options3LO) client() *http.Client {
if o.Client != nil {
return o.Client
}
return internal.CloneDefaultClient()
}
// authCodeURL returns a URL that points to a OAuth2 consent page.
func (o *Options3LO) authCodeURL(state string, values url.Values) string {
var buf bytes.Buffer
buf.WriteString(o.AuthURL)
v := url.Values{
"response_type": {"code"},
"client_id": {o.ClientID},
}
if o.RedirectURL != "" {
v.Set("redirect_uri", o.RedirectURL)
}
if len(o.Scopes) > 0 {
v.Set("scope", strings.Join(o.Scopes, " "))
}
if state != "" {
v.Set("state", state)
}
if o.AuthHandlerOpts != nil {
if o.AuthHandlerOpts.PKCEOpts != nil &&
o.AuthHandlerOpts.PKCEOpts.Challenge != "" {
v.Set(codeChallengeKey, o.AuthHandlerOpts.PKCEOpts.Challenge)
}
if o.AuthHandlerOpts.PKCEOpts != nil &&
o.AuthHandlerOpts.PKCEOpts.ChallengeMethod != "" {
v.Set(codeChallengeMethodKey, o.AuthHandlerOpts.PKCEOpts.ChallengeMethod)
}
}
for k := range values {
v.Set(k, v.Get(k))
}
if strings.Contains(o.AuthURL, "?") {
buf.WriteByte('&')
} else {
buf.WriteByte('?')
}
buf.WriteString(v.Encode())
return buf.String()
}
// New3LOTokenProvider returns a [TokenProvider] based on the 3-legged OAuth2
// configuration. The TokenProvider is caches and auto-refreshes tokens by
// default.
func New3LOTokenProvider(opts *Options3LO) (TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
if opts.AuthHandlerOpts != nil {
return new3LOTokenProviderWithAuthHandler(opts), nil
}
return NewCachedTokenProvider(&tokenProvider3LO{opts: opts, refreshToken: opts.RefreshToken, client: opts.client()}, &CachedTokenProviderOptions{
ExpireEarly: opts.EarlyTokenExpiry,
}), nil
}
// AuthorizationHandlerOptions provides a set of options to specify for doing a
// 3-legged OAuth2 flow with a custom [AuthorizationHandler].
type AuthorizationHandlerOptions struct {
// AuthorizationHandler specifies the handler used to for the authorization
// part of the flow.
Handler AuthorizationHandler
// State is used verify that the "state" is identical in the request and
// response before exchanging the auth code for OAuth2 token.
State string
// PKCEOpts allows setting configurations for PKCE. Optional.
PKCEOpts *PKCEOptions
}
func new3LOTokenProviderWithAuthHandler(opts *Options3LO) TokenProvider {
return NewCachedTokenProvider(&tokenProviderWithHandler{opts: opts, state: opts.AuthHandlerOpts.State}, &CachedTokenProviderOptions{
ExpireEarly: opts.EarlyTokenExpiry,
})
}
// exchange handles the final exchange portion of the 3lo flow. Returns a Token,
// refreshToken, and error.
func (o *Options3LO) exchange(ctx context.Context, code string) (*Token, string, error) {
// Build request
v := url.Values{
"grant_type": {"authorization_code"},
"code": {code},
}
if o.RedirectURL != "" {
v.Set("redirect_uri", o.RedirectURL)
}
if o.AuthHandlerOpts != nil &&
o.AuthHandlerOpts.PKCEOpts != nil &&
o.AuthHandlerOpts.PKCEOpts.Verifier != "" {
v.Set(codeVerifierKey, o.AuthHandlerOpts.PKCEOpts.Verifier)
}
for k := range o.URLParams {
v.Set(k, o.URLParams.Get(k))
}
return fetchToken(ctx, o, v)
}
// This struct is not safe for concurrent access alone, but the way it is used
// in this package by wrapping it with a cachedTokenProvider makes it so.
type tokenProvider3LO struct {
opts *Options3LO
client *http.Client
refreshToken string
}
func (tp *tokenProvider3LO) Token(ctx context.Context) (*Token, error) {
if tp.refreshToken == "" {
return nil, errors.New("auth: token expired and refresh token is not set")
}
v := url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {tp.refreshToken},
}
for k := range tp.opts.URLParams {
v.Set(k, tp.opts.URLParams.Get(k))
}
tk, rt, err := fetchToken(ctx, tp.opts, v)
if err != nil {
return nil, err
}
if tp.refreshToken != rt && rt != "" {
tp.refreshToken = rt
}
return tk, err
}
type tokenProviderWithHandler struct {
opts *Options3LO
state string
}
func (tp tokenProviderWithHandler) Token(ctx context.Context) (*Token, error) {
url := tp.opts.authCodeURL(tp.state, nil)
code, state, err := tp.opts.AuthHandlerOpts.Handler(url)
if err != nil {
return nil, err
}
if state != tp.state {
return nil, errors.New("auth: state mismatch in 3-legged-OAuth flow")
}
tok, _, err := tp.opts.exchange(ctx, code)
return tok, err
}
// fetchToken returns a Token, refresh token, and/or an error.
func fetchToken(ctx context.Context, o *Options3LO, v url.Values) (*Token, string, error) {
var refreshToken string
if o.AuthStyle == StyleInParams {
if o.ClientID != "" {
v.Set("client_id", o.ClientID)
}
if o.ClientSecret != "" {
v.Set("client_secret", o.ClientSecret)
}
}
req, err := http.NewRequest("POST", o.TokenURL, strings.NewReader(v.Encode()))
if err != nil {
return nil, refreshToken, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if o.AuthStyle == StyleInHeader {
req.SetBasicAuth(url.QueryEscape(o.ClientID), url.QueryEscape(o.ClientSecret))
}
// Make request
r, err := o.client().Do(req.WithContext(ctx))
if err != nil {
return nil, refreshToken, err
}
body, err := internal.ReadAll(r.Body)
r.Body.Close()
if err != nil {
return nil, refreshToken, fmt.Errorf("auth: cannot fetch token: %w", err)
}
failureStatus := r.StatusCode < 200 || r.StatusCode > 299
tokError := &Error{
Response: r,
Body: body,
}
var token *Token
// errors ignored because of default switch on content
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
switch content {
case "application/x-www-form-urlencoded", "text/plain":
// some endpoints return a query string
vals, err := url.ParseQuery(string(body))
if err != nil {
if failureStatus {
return nil, refreshToken, tokError
}
return nil, refreshToken, fmt.Errorf("auth: cannot parse response: %w", err)
}
tokError.code = vals.Get("error")
tokError.description = vals.Get("error_description")
tokError.uri = vals.Get("error_uri")
token = &Token{
Value: vals.Get("access_token"),
Type: vals.Get("token_type"),
Metadata: make(map[string]interface{}, len(vals)),
}
for k, v := range vals {
token.Metadata[k] = v
}
refreshToken = vals.Get("refresh_token")
e := vals.Get("expires_in")
expires, _ := strconv.Atoi(e)
if expires != 0 {
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
}
default:
var tj tokenJSON
if err = json.Unmarshal(body, &tj); err != nil {
if failureStatus {
return nil, refreshToken, tokError
}
return nil, refreshToken, fmt.Errorf("auth: cannot parse json: %w", err)
}
tokError.code = tj.ErrorCode
tokError.description = tj.ErrorDescription
tokError.uri = tj.ErrorURI
token = &Token{
Value: tj.AccessToken,
Type: tj.TokenType,
Expiry: tj.expiry(),
Metadata: make(map[string]interface{}),
}
json.Unmarshal(body, &token.Metadata) // optional field, skip err check
refreshToken = tj.RefreshToken
}
// according to spec, servers should respond status 400 in error case
// https://www.rfc-editor.org/rfc/rfc6749#section-5.2
// but some unorthodox servers respond 200 in error case
if failureStatus || tokError.code != "" {
return nil, refreshToken, tokError
}
if token.Value == "" {
return nil, refreshToken, errors.New("auth: server response missing access_token")
}
return token, refreshToken, nil
}

View file

@ -15,6 +15,8 @@ import (
"os"
"time"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/oauth2adapt"
"golang.org/x/oauth2"
"google.golang.org/api/internal/cert"
"google.golang.org/api/internal/impersonate"
@ -27,6 +29,9 @@ const quotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
// Creds returns credential information obtained from DialSettings, or if none, then
// it returns default credential information.
func Creds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
if ds.IsNewAuthLibraryEnabled() {
return credsNewAuth(ctx, ds)
}
creds, err := baseCreds(ctx, ds)
if err != nil {
return nil, err
@ -37,6 +42,59 @@ func Creds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
return creds, nil
}
func credsNewAuth(ctx context.Context, settings *DialSettings) (*google.Credentials, error) {
// Preserve old options behavior
if settings.InternalCredentials != nil {
return settings.InternalCredentials, nil
} else if settings.Credentials != nil {
return settings.Credentials, nil
} else if settings.TokenSource != nil {
return &google.Credentials{TokenSource: settings.TokenSource}, nil
}
if settings.AuthCredentials != nil {
return oauth2adapt.Oauth2CredentialsFromAuthCredentials(settings.AuthCredentials), nil
}
var useSelfSignedJWT bool
var aud string
var scopes []string
// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
if settings.EnableJwtWithScope || len(settings.Audiences) > 0 {
useSelfSignedJWT = true
}
if len(settings.Scopes) > 0 {
scopes = make([]string, len(settings.Scopes))
copy(scopes, settings.Scopes)
}
if len(settings.Audiences) > 0 {
aud = settings.Audiences[0]
}
// Only default scopes if user did not also set an audience.
if len(settings.Scopes) == 0 && aud == "" && len(settings.DefaultScopes) > 0 {
scopes = make([]string, len(settings.DefaultScopes))
copy(scopes, settings.DefaultScopes)
}
if len(scopes) == 0 && aud == "" {
aud = settings.DefaultAudience
}
creds, err := credentials.DetectDefault(&credentials.DetectOptions{
Scopes: scopes,
Audience: aud,
CredentialsFile: settings.CredentialsFile,
CredentialsJSON: settings.CredentialsJSON,
UseSelfSignedJWT: useSelfSignedJWT,
Client: oauth2.NewClient(ctx, nil),
})
if err != nil {
return nil, err
}
return oauth2adapt.Oauth2CredentialsFromAuthCredentials(creds), nil
}
func baseCreds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
if ds.InternalCredentials != nil {
return ds.InternalCredentials, nil

View file

@ -13,6 +13,7 @@ import (
"strconv"
"time"
"cloud.google.com/go/auth"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/internal/impersonate"
@ -20,9 +21,10 @@ import (
)
const (
newAuthLibEnvVar = "GOOGLE_API_GO_EXPERIMENTAL_USE_NEW_AUTH_LIB"
universeDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
defaultUniverseDomain = "googleapis.com"
newAuthLibEnvVar = "GOOGLE_API_GO_EXPERIMENTAL_ENABLE_NEW_AUTH_LIB"
newAuthLibDisabledEnVar = "GOOGLE_API_GO_EXPERIMENTAL_DISABLE_NEW_AUTH_LIB"
universeDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
defaultUniverseDomain = "googleapis.com"
)
// DialSettings holds information needed to establish a connection with a
@ -57,7 +59,6 @@ type DialSettings struct {
ImpersonationConfig *impersonate.Config
EnableDirectPath bool
EnableDirectPathXds bool
EnableNewAuthLibrary bool
AllowNonDefaultServiceAccount bool
DefaultUniverseDomain string
UniverseDomain string
@ -65,6 +66,10 @@ type DialSettings struct {
// https://cloud.google.com/apis/docs/system-parameters
QuotaProject string
RequestReason string
// New Auth library Options
AuthCredentials *auth.Credentials
EnableNewAuthLibrary bool
}
// GetScopes returns the user-provided scopes, if set, or else falls back to the
@ -91,6 +96,11 @@ func (ds *DialSettings) HasCustomAudience() bool {
// IsNewAuthLibraryEnabled returns true if the new auth library should be used.
func (ds *DialSettings) IsNewAuthLibraryEnabled() bool {
// Disabled env is for future rollouts to make sure there is a way to easily
// disable this behaviour once we switch in on by default.
if b, err := strconv.ParseBool(os.Getenv(newAuthLibDisabledEnVar)); err == nil && b {
return false
}
if ds.EnableNewAuthLibrary {
return true
}

View file

@ -5,4 +5,4 @@
package internal
// Version is the current tagged release of the library.
const Version = "0.173.0"
const Version = "0.174.0"

View file

@ -9,6 +9,7 @@ import (
"crypto/tls"
"net/http"
"cloud.google.com/go/auth"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/internal"
@ -344,6 +345,19 @@ func WithCredentials(creds *google.Credentials) ClientOption {
return (*withCreds)(creds)
}
// WithAuthCredentials returns a ClientOption that specifies an
// [cloud.google.com/go/auth.Credentials] to be used as the basis for
// authentication.
func WithAuthCredentials(creds *auth.Credentials) ClientOption {
return withAuthCredentials{creds}
}
type withAuthCredentials struct{ creds *auth.Credentials }
func (w withAuthCredentials) Apply(o *internal.DialSettings) {
o.AuthCredentials = w.creds
}
// WithUniverseDomain returns a ClientOption that sets the universe domain.
//
// This is an EXPERIMENTAL API and may be changed or removed in the future.

View file

@ -17,6 +17,10 @@ import (
"sync"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/grpctransport"
"cloud.google.com/go/auth/oauth2adapt"
"cloud.google.com/go/compute/metadata"
"go.opencensus.io/plugin/ocgrpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
@ -79,6 +83,13 @@ func Dial(ctx context.Context, opts ...option.ClientOption) (*grpc.ClientConn, e
if o.GRPCConnPool != nil {
return o.GRPCConnPool.Conn(), nil
}
if o.IsNewAuthLibraryEnabled() {
pool, err := dialPoolNewAuth(ctx, true, 1, o)
if err != nil {
return nil, err
}
return pool.Connection(), nil
}
// NOTE(cbro): We removed support for option.WithGRPCConnPool (GRPCConnPoolSize)
// on 2020-02-12 because RoundRobin and WithBalancer are deprecated and we need to remove usages of it.
//
@ -94,6 +105,13 @@ func DialInsecure(ctx context.Context, opts ...option.ClientOption) (*grpc.Clien
if err != nil {
return nil, err
}
if o.IsNewAuthLibraryEnabled() {
pool, err := dialPoolNewAuth(ctx, false, 1, o)
if err != nil {
return nil, err
}
return pool.Connection(), nil
}
return dial(ctx, true, o)
}
@ -112,6 +130,18 @@ func DialPool(ctx context.Context, opts ...option.ClientOption) (ConnPool, error
if o.GRPCConnPool != nil {
return o.GRPCConnPool, nil
}
if o.IsNewAuthLibraryEnabled() {
if o.GRPCConn != nil {
return &singleConnPool{o.GRPCConn}, nil
}
pool, err := dialPoolNewAuth(ctx, true, o.GRPCConnPoolSize, o)
if err != nil {
return nil, err
}
return &poolAdapter{pool}, nil
}
poolSize := o.GRPCConnPoolSize
if o.GRPCConn != nil {
// WithGRPCConn is technically incompatible with WithGRPCConnectionPool.
@ -141,6 +171,66 @@ func DialPool(ctx context.Context, opts ...option.ClientOption) (ConnPool, error
return pool, nil
}
// dialPoolNewAuth is an adapter to call new auth library.
func dialPoolNewAuth(ctx context.Context, secure bool, poolSize int, ds *internal.DialSettings) (grpctransport.GRPCClientConnPool, error) {
// honor options if set
var ts oauth2.TokenSource
if ds.InternalCredentials != nil {
ts = ds.InternalCredentials.TokenSource
} else if ds.Credentials != nil {
ts = ds.Credentials.TokenSource
} else if ds.TokenSource != nil {
ts = ds.TokenSource
}
var creds *auth.Credentials
if ds.AuthCredentials != nil {
creds = ds.AuthCredentials
} else if ts != nil {
creds = auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: oauth2adapt.TokenProviderFromTokenSource(ts),
})
}
var aud string
if len(ds.Audiences) > 0 {
aud = ds.Audiences[0]
}
metadata := map[string]string{}
if ds.QuotaProject != "" {
metadata["X-goog-user-project"] = ds.QuotaProject
}
if ds.RequestReason != "" {
metadata["X-goog-request-reason"] = ds.RequestReason
}
pool, err := grpctransport.Dial(ctx, secure, &grpctransport.Options{
DisableTelemetry: ds.TelemetryDisabled,
DisableAuthentication: ds.NoAuth,
Endpoint: ds.Endpoint,
Metadata: metadata,
GRPCDialOpts: ds.GRPCDialOpts,
PoolSize: poolSize,
Credentials: creds,
DetectOpts: &credentials.DetectOptions{
Scopes: ds.Scopes,
Audience: aud,
CredentialsFile: ds.CredentialsFile,
CredentialsJSON: ds.CredentialsJSON,
Client: oauth2.NewClient(ctx, nil),
},
InternalOptions: &grpctransport.InternalOptions{
EnableNonDefaultSAForDirectPath: ds.AllowNonDefaultServiceAccount,
EnableDirectPath: ds.EnableDirectPath,
EnableDirectPathXds: ds.EnableDirectPathXds,
EnableJWTWithScope: ds.EnableJwtWithScope,
DefaultAudience: ds.DefaultAudience,
DefaultEndpointTemplate: ds.DefaultEndpointTemplate,
DefaultMTLSEndpoint: ds.DefaultMTLSEndpoint,
DefaultScopes: ds.DefaultScopes,
},
})
return pool, err
}
func dial(ctx context.Context, insecure bool, o *internal.DialSettings) (*grpc.ClientConn, error) {
if o.HTTPClient != nil {
return nil, errors.New("unsupported HTTP client specified")

View file

@ -9,6 +9,7 @@ import (
"fmt"
"sync/atomic"
"cloud.google.com/go/auth/grpctransport"
"google.golang.org/api/internal"
"google.golang.org/grpc"
)
@ -90,3 +91,27 @@ func (m multiError) Error() string {
}
return fmt.Sprintf("%s (and %d other errors)", s, n-1)
}
type poolAdapter struct {
pool grpctransport.GRPCClientConnPool
}
func (p *poolAdapter) Conn() *grpc.ClientConn {
return p.pool.Connection()
}
func (p *poolAdapter) Num() int {
return p.pool.Len()
}
func (p *poolAdapter) Close() error {
return p.pool.Close()
}
func (p *poolAdapter) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
return p.Conn().Invoke(ctx, method, args, reply, opts...)
}
func (p *poolAdapter) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return p.Conn().NewStream(ctx, desc, method, opts...)
}

View file

@ -15,6 +15,10 @@ import (
"net/http"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/httptransport"
"cloud.google.com/go/auth/oauth2adapt"
"go.opencensus.io/plugin/ochttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"golang.org/x/net/http2"
@ -43,6 +47,13 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*http.Client,
return settings.HTTPClient, endpoint, nil
}
if settings.IsNewAuthLibraryEnabled() {
client, err := newClientNewAuth(ctx, settings)
if err != nil {
return nil, "", err
}
return client, endpoint, nil
}
trans, err := newTransport(ctx, defaultBaseTransport(ctx, clientCertSource, dialTLSContext), settings)
if err != nil {
return nil, "", err
@ -50,6 +61,66 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*http.Client,
return &http.Client{Transport: trans}, endpoint, nil
}
// newClientNewAuth is an adapter to call new auth library.
func newClientNewAuth(ctx context.Context, settings *internal.DialSettings) (*http.Client, error) {
// honor options if set
var ts oauth2.TokenSource
if settings.InternalCredentials != nil {
ts = settings.InternalCredentials.TokenSource
} else if settings.Credentials != nil {
ts = settings.Credentials.TokenSource
} else if settings.TokenSource != nil {
ts = settings.TokenSource
}
var creds *auth.Credentials
if settings.AuthCredentials != nil {
creds = settings.AuthCredentials
} else if ts != nil {
creds = auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: oauth2adapt.TokenProviderFromTokenSource(ts),
})
}
var aud string
if len(settings.Audiences) > 0 {
aud = settings.Audiences[0]
}
headers := http.Header{}
if settings.QuotaProject != "" {
headers.Set("X-goog-user-project", settings.QuotaProject)
}
if settings.RequestReason != "" {
headers.Set("X-goog-request-reason", settings.RequestReason)
}
client, err := httptransport.NewClient(&httptransport.Options{
DisableTelemetry: settings.TelemetryDisabled,
DisableAuthentication: settings.NoAuth,
Headers: headers,
Endpoint: settings.Endpoint,
APIKey: settings.APIKey,
Credentials: creds,
ClientCertProvider: settings.ClientCertSource,
DetectOpts: &credentials.DetectOptions{
Scopes: settings.Scopes,
Audience: aud,
CredentialsFile: settings.CredentialsFile,
CredentialsJSON: settings.CredentialsJSON,
Client: oauth2.NewClient(ctx, nil),
},
InternalOptions: &httptransport.InternalOptions{
EnableJWTWithScope: settings.EnableJwtWithScope,
DefaultAudience: settings.DefaultAudience,
DefaultEndpointTemplate: settings.DefaultEndpointTemplate,
DefaultMTLSEndpoint: settings.DefaultMTLSEndpoint,
DefaultScopes: settings.DefaultScopes,
},
})
if err != nil {
return nil, err
}
return client, nil
}
// NewTransport creates an http.RoundTripper for use communicating with a Google
// cloud service, configured with the given ClientOptions. Its RoundTrip method delegates to base.
func NewTransport(ctx context.Context, base http.RoundTripper, opts ...option.ClientOption) (http.RoundTripper, error) {
@ -60,6 +131,14 @@ func NewTransport(ctx context.Context, base http.RoundTripper, opts ...option.Cl
if settings.HTTPClient != nil {
return nil, errors.New("transport/http: WithHTTPClient passed to NewTransport")
}
if settings.IsNewAuthLibraryEnabled() {
// TODO, this is not wrapping the base, find a way...
client, err := newClientNewAuth(ctx, settings)
if err != nil {
return nil, err
}
return client.Transport, nil
}
return newTransport(ctx, base, settings)
}

21
vendor/modules.txt vendored
View file

@ -4,6 +4,25 @@ cloud.google.com/go/internal
cloud.google.com/go/internal/optional
cloud.google.com/go/internal/trace
cloud.google.com/go/internal/version
# cloud.google.com/go/auth v0.2.0
## explicit; go 1.19
cloud.google.com/go/auth
cloud.google.com/go/auth/credentials
cloud.google.com/go/auth/credentials/internal/externalaccount
cloud.google.com/go/auth/credentials/internal/externalaccountuser
cloud.google.com/go/auth/credentials/internal/gdch
cloud.google.com/go/auth/credentials/internal/impersonate
cloud.google.com/go/auth/credentials/internal/stsexchange
cloud.google.com/go/auth/grpctransport
cloud.google.com/go/auth/httptransport
cloud.google.com/go/auth/internal
cloud.google.com/go/auth/internal/credsfile
cloud.google.com/go/auth/internal/jwt
cloud.google.com/go/auth/internal/transport
cloud.google.com/go/auth/internal/transport/cert
# cloud.google.com/go/auth/oauth2adapt v0.2.0
## explicit; go 1.19
cloud.google.com/go/auth/oauth2adapt
# cloud.google.com/go/compute/metadata v0.3.0
## explicit; go 1.19
cloud.google.com/go/compute/metadata
@ -701,7 +720,7 @@ golang.org/x/text/unicode/norm
# golang.org/x/time v0.5.0
## explicit; go 1.18
golang.org/x/time/rate
# google.golang.org/api v0.173.0
# google.golang.org/api v0.174.0
## explicit; go 1.19
google.golang.org/api/googleapi
google.golang.org/api/googleapi/transport