Skip to content

Commit

Permalink
feat(auth): add Credentials.UniverseDomain() (#8654)
Browse files Browse the repository at this point in the history
Read and expose universe_domain from self-signed service account JSON
  • Loading branch information
quartzmo committed Oct 5, 2023
1 parent 182e6fe commit af0aa1e
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 18 deletions.
20 changes: 17 additions & 3 deletions auth/detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const (

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

universeDomainDefault = "googleapis.com"
)

var (
Expand All @@ -50,16 +52,19 @@ 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) *Credentials {
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,
}
}

Expand All @@ -81,6 +86,15 @@ 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
Expand Down Expand Up @@ -122,7 +136,7 @@ func DefaultCredentials(opts *Options) (*Credentials, error) {

if OnGCE() {
id, _ := metadata.ProjectID()
return newCredentials(computeTokenProvider(opts.EarlyTokenRefresh, opts.Scopes...), nil, id, ""), nil
return newCredentials(computeTokenProvider(opts.EarlyTokenRefresh, opts.Scopes...), nil, id, "", ""), nil
}

return nil, fmt.Errorf("detect: could not find default credentials. See %v for more information", adcSetupURL)
Expand Down Expand Up @@ -204,7 +218,7 @@ func readCredentialsFileJSON(b []byte, opts *Options) (*Credentials, error) {
if err != nil {
return nil, err
}
return newCredentials(tp, b, "", ""), nil
return newCredentials(tp, b, "", "", ""), nil
}
return fileCredentials(b, opts)
}
Expand Down
104 changes: 100 additions & 4 deletions auth/detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,21 @@ func TestDefaultCredentials_GdchServiceAccountKey(t *testing.T) {
if _, err := DefaultCredentials(&Options{CredentialsJSON: b}); err == nil {
t.Fatal("STSAudience should be required")
}
cred, err := DefaultCredentials(&Options{
creds, err := DefaultCredentials(&Options{
CredentialsJSON: b,
STSAudience: aud,
})
if err != nil {
t.Fatal(err)
}

if want := "fake_project"; cred.ProjectID() != want {
t.Fatalf("got %q, want %q", cred.ProjectID(), want)
if want := "fake_project"; creds.ProjectID() != want {
t.Fatalf("got %q, want %q", creds.ProjectID(), want)
}
tok, err := cred.Token(context.Background())
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -173,6 +176,9 @@ func TestDefaultCredentials_ImpersonatedServiceAccountKey(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
Expand Down Expand Up @@ -209,6 +215,48 @@ func TestDefaultCredentials_UserCredentialsKey(t *testing.T) {
if want := "fake_project2"; creds.QuotaProjectID() != want {
t.Fatalf("got %q, want %q", creds.ProjectID(), want)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
}
if want := "a_fake_token"; tok.Value != want {
t.Fatalf("got %q, want %q", tok.Value, want)
}
if want := internal.TokenTypeBearer; tok.Type != want {
t.Fatalf("got %q, want %q", tok.Type, want)
}
}

func TestDefaultCredentials_UserCredentialsKey_UniverseDomain(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := &tokResp{
AccessToken: "a_fake_token",
TokenType: internal.TokenTypeBearer,
ExpiresIn: 60,
}
if err := json.NewEncoder(w).Encode(&resp); err != nil {
t.Fatal(err)
}
}))

creds, err := DefaultCredentials(&Options{
CredentialsFile: "../internal/testdata/user_universe_domain.json",
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
TokenURL: ts.URL,
})
if err != nil {
t.Fatal(err)
}
if want := "fake_project2"; creds.QuotaProjectID() != want {
t.Fatalf("got %q, want %q", creds.ProjectID(), want)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
Expand Down Expand Up @@ -256,6 +304,9 @@ func TestDefaultCredentials_ServiceAccountKey(t *testing.T) {
if want := "fake_project"; creds.ProjectID() != want {
t.Fatalf("got %q, want %q", creds.ProjectID(), want)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
Expand Down Expand Up @@ -289,6 +340,45 @@ func TestDefaultCredentials_ServiceAccountKeySelfSigned(t *testing.T) {
if want := "fake_project"; creds.ProjectID() != want {
t.Fatalf("got %q, want %q", creds.ProjectID(), want)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
}
if tok.Value != wantTok {
t.Fatalf("got %q, want %q", tok.Value, wantTok)
}
if want := internal.TokenTypeBearer; tok.Type != want {
t.Fatalf("got %q, want %q", tok.Type, want)
}
}

func TestDefaultCredentials_ServiceAccountKeySelfSigned_UniverseDomain(t *testing.T) {
b, err := os.ReadFile("../internal/testdata/sa_universe_domain.json")
if err != nil {
t.Fatal(err)
}
oldNow := now
now = func() time.Time { return time.Date(2000, 2, 1, 12, 30, 0, 0, time.UTC) }
defer func() { now = oldNow }()
wantTok := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiY2RlZjEyMzQ1Njc4OTAifQ.eyJpc3MiOiJnb3BoZXJAZmFrZV9wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwic2NvcGUiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9hdXRoL2Nsb3VkLXBsYXRmb3JtIiwiZXhwIjo5NDk0MTE4MDAsImlhdCI6OTQ5NDA4MjAwLCJhdWQiOiIiLCJzdWIiOiJnb3BoZXJAZmFrZV9wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIn0.n9Hggd-1Vw4WTQiWkh7q9r5eDsz-khU5vwkZl2VmgdUF3ZxDq1ARzchCNtTifeorzbp9C0i0vCr855G7FZkVCJXPVMcnxbwfMSafUYmVsmutbQiV9eTWfWM0_Ljiwa9GEbv1bN06Lz4LrelPKEaxsDbY6tU8LJUiome_gSMLfLk"

creds, err := DefaultCredentials(&Options{
CredentialsJSON: b,
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
UseSelfSignedJWT: true,
})
if err != nil {
t.Fatal(err)
}
if want := "fake_project"; creds.ProjectID() != want {
t.Fatalf("got %q, want %q", creds.ProjectID(), want)
}
if want := "example.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
Expand Down Expand Up @@ -347,6 +437,9 @@ func TestDefaultCredentials_ClientCredentials(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
Expand Down Expand Up @@ -431,6 +524,9 @@ func TestDefaultCredentials_ExternalAccountKey(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if want := "googleapis.com"; creds.UniverseDomain() != want {
t.Fatalf("got %q, want %q", creds.UniverseDomain(), want)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
Expand Down
7 changes: 5 additions & 2 deletions auth/detect/filetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func fileCredentials(b []byte, opts *Options) (*Credentials, error) {
return nil, err
}

var projectID, quotaProjectID string
var projectID, quotaProjectID, universeDomain string
var tp auth.TokenProvider
switch fileType {
case internaldetect.ServiceAccountKey:
Expand All @@ -44,6 +44,7 @@ func fileCredentials(b []byte, opts *Options) (*Credentials, error) {
return nil, err
}
projectID = f.ProjectID
universeDomain = f.UniverseDomain
case internaldetect.UserCredentialsKey:
f, err := internaldetect.ParseUserCredentials(b)
if err != nil {
Expand All @@ -64,6 +65,7 @@ func fileCredentials(b []byte, opts *Options) (*Credentials, error) {
return nil, err
}
quotaProjectID = f.QuotaProjectID
universeDomain = f.UniverseDomain
case internaldetect.ImpersonatedServiceAccountKey:
f, err := internaldetect.ParseImpersonatedServiceAccount(b)
if err != nil {
Expand All @@ -73,6 +75,7 @@ func fileCredentials(b []byte, opts *Options) (*Credentials, error) {
if err != nil {
return nil, err
}
universeDomain = f.UniverseDomain
case internaldetect.GDCHServiceAccountKey:
f, err := internaldetect.ParseGDCHServiceAccount(b)
if err != nil {
Expand All @@ -88,7 +91,7 @@ func fileCredentials(b []byte, opts *Options) (*Credentials, error) {
}
return newCredentials(auth.NewCachedTokenProvider(tp, &auth.CachedTokenProviderOptions{
ExpireEarly: opts.EarlyTokenRefresh,
}), b, projectID, quotaProjectID), nil
}), b, projectID, quotaProjectID, universeDomain), nil
}

func handleServiceAccount(f *internaldetect.ServiceAccountFile, opts *Options) (auth.TokenProvider, error) {
Expand Down
21 changes: 12 additions & 9 deletions auth/internal/internaldetect/filetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ type ClientCredentialsFile struct {

// 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"`
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.
Expand All @@ -66,6 +67,7 @@ type ExternalAccountFile struct {
ServiceAccountImpersonation ServiceAccountImpersonationInfo `json:"service_account_impersonation"`
QuotaProjectID string `json:"quota_project_id"`
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
UniverseDomain string `json:"universe_domain"`
}

// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
Expand Down Expand Up @@ -112,9 +114,10 @@ type ImpersonatedServiceAccountFile struct {
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
Delegates []string `json:"delegates"`
CredSource json.RawMessage `json:"source_credentials"`
UniverseDomain string `json:"universe_domain"`
}

// GDCHServiceAccountFile representation.
// GDCHServiceAccountFile represents the Google Distributed Cloud Hosted (GDCH) service identity file.
type GDCHServiceAccountFile struct {
Type string `json:"type"`
FormatVersion string `json:"format_version"`
Expand Down
13 changes: 13 additions & 0 deletions auth/internal/testdata/sa_universe_domain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "fake_project",
"universe_domain": "example.com",
"private_key_id": "abcdef1234567890",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12ikv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/GrCtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrPSXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAutLPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEAgidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ==\n-----END PRIVATE KEY-----\n",
"client_email": "gopher@fake_project.iam.gserviceaccount.com",
"client_id": "gopher",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gopher%40fake_project.iam.gserviceaccount.com"
}
8 changes: 8 additions & 0 deletions auth/internal/testdata/user_universe_domain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"client_id": "abc123.apps.googleusercontent.com",
"client_secret": "shh",
"refresh_token": "refreshing",
"type": "authorized_user",
"quota_project_id": "fake_project2",
"universe_domain": "example.com"
}

0 comments on commit af0aa1e

Please sign in to comment.