Skip to content

Commit

Permalink
feat(auth): move credentials to base auth package (#9590)
Browse files Browse the repository at this point in the history
We are moving the credentials struct to the base auth package as
we intend to use this as the main abstraction for this library.
To better support the new idea of universes having access to the
full credential object will be crucial for the client libraries.
Also, the detect package has been renamed to `credentials` to make
it more generic in the future. We have plans on directly exposing
packages like `externalaccount` in the future so having a better
name here makes sense.

This is the first of two large breaking changes for this module.
This module not in use by our libraries yet, is version 0.1.X and
has always been explicitly labeled experimental in the readme. As
such, we feel comfortable making these changes for the long term
health of the module.
  • Loading branch information
codyoss committed Mar 15, 2024
1 parent 93c7bc0 commit 1a04baf
Show file tree
Hide file tree
Showing 46 changed files with 390 additions and 227 deletions.
108 changes: 108 additions & 0 deletions auth/auth.go
Expand Up @@ -40,6 +40,8 @@ const (
// 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 (
Expand Down Expand Up @@ -94,6 +96,112 @@ func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
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 {
Expand Down
2 changes: 1 addition & 1 deletion auth/detect/compute.go → auth/credentials/compute.go
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package detect
package credentials

import (
"context"
Expand Down
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package detect
package credentials

import (
"context"
Expand Down
91 changes: 24 additions & 67 deletions auth/detect/detect.go → auth/credentials/detect.go
Expand Up @@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package detect
package credentials

import (
"context"
"encoding/json"
"errors"
"fmt"
Expand All @@ -38,72 +39,21 @@ const (

// Help on default credentials
adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"

universeDomainDefault = "googleapis.com"
)

var (
// for testing
allowOnGCECheck = true
)

// Credentials holds Google credentials, including
// [Application Default Credentials](https://developers.google.com/accounts/docs/application-default-credentials).
type Credentials struct {
json []byte
projectID string
quotaProjectID string
// universeDomain is the default service domain for a given Cloud universe.
universeDomain string

auth.TokenProvider
}

func newCredentials(tokenProvider auth.TokenProvider, json []byte, projectID string, quotaProjectID string, universeDomain string) *Credentials {
return &Credentials{
json: json,
projectID: internal.GetProjectID(json, projectID),
quotaProjectID: internal.GetQuotaProject(json, quotaProjectID),
TokenProvider: tokenProvider,
universeDomain: universeDomain,
}
}

// 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() string {
return c.projectID
}

// QuotaProjectID returns the associated quota project ID from the underlying
// file or environment.
func (c *Credentials) QuotaProjectID() string {
return c.quotaProjectID
}

// UniverseDomain returns the default service domain for a given Cloud universe.
// The default value is "googleapis.com".
func (c *Credentials) UniverseDomain() string {
if c.universeDomain == "" {
return universeDomainDefault
}
return c.universeDomain
}

// 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()
}

// DefaultCredentials searches for "Application Default Credentials" and returns
// a credential based on the [Options] provided.
// 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:
Expand All @@ -119,7 +69,7 @@ func OnGCE() bool {
// - 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 DefaultCredentials(opts *Options) (*Credentials, error) {
func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) {
if err := opts.validate(); err != nil {
return nil, err
}
Expand All @@ -138,15 +88,19 @@ func DefaultCredentials(opts *Options) (*Credentials, error) {
}

if OnGCE() {
id, _ := metadata.ProjectID()
return newCredentials(computeTokenProvider(opts.EarlyTokenRefresh, opts.Scopes...), nil, id, "", ""), nil
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: computeTokenProvider(opts.EarlyTokenRefresh, opts.Scopes...),
ProjectIDProvider: auth.CredentialsPropertyFunc(func(context.Context) (string, error) {
return metadata.ProjectID()
}),
}), nil
}

return nil, fmt.Errorf("detect: could not find default credentials. See %v for more information", adcSetupURL)
}

// Options provides configuration for [DefaultCredentials].
type Options struct {
// 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.
Expand Down Expand Up @@ -188,7 +142,7 @@ type Options struct {
Client *http.Client
}

func (o *Options) validate() error {
func (o *DetectOptions) validate() error {
if o == nil {
return errors.New("detect: options must be provided")
}
Expand All @@ -201,35 +155,35 @@ func (o *Options) validate() error {
return nil
}

func (o *Options) tokenURL() string {
func (o *DetectOptions) tokenURL() string {
if o.TokenURL != "" {
return o.TokenURL
}
return googleTokenURL
}

func (o *Options) scopes() []string {
func (o *DetectOptions) scopes() []string {
scopes := make([]string, len(o.Scopes))
copy(scopes, o.Scopes)
return scopes
}

func (o *Options) client() *http.Client {
func (o *DetectOptions) client() *http.Client {
if o.Client != nil {
return o.Client
}
return internal.CloneDefaultClient()
}

func readCredentialsFile(filename string, opts *Options) (*Credentials, error) {
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 *Options) (*Credentials, error) {
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 {
Expand All @@ -240,12 +194,15 @@ func readCredentialsFileJSON(b []byte, opts *Options) (*Credentials, error) {
if err != nil {
return nil, err
}
return newCredentials(tp, b, "", "", ""), nil
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: tp,
JSON: b,
}), nil
}
return fileCredentials(b, opts)
}

func clientCredConfigFromJSON(b []byte, opts *Options) *auth.Options3LO {
func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO {
var creds internaldetect.ClientCredentialsFile
var c *internaldetect.Config3LO
if err := json.Unmarshal(b, &creds); err != nil {
Expand Down

0 comments on commit 1a04baf

Please sign in to comment.