Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storage): find GoogleAccessID when using impersonated creds #6591

Merged
merged 17 commits into from Sep 14, 2022
Merged
107 changes: 75 additions & 32 deletions storage/bucket.go
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"reflect"
"strings"
"time"

"cloud.google.com/go/compute/metadata"
Expand Down Expand Up @@ -159,22 +160,38 @@ func (b *BucketHandle) Update(ctx context.Context, uattrs BucketAttrsToUpdate) (
}

// SignedURL returns a URL for the specified object. Signed URLs allow anyone
// access to a restricted resource for a limited time without needing a
// Google account or signing in. For more information about signed URLs, see
// https://cloud.google.com/storage/docs/accesscontrol#signed_urls_query_string_authentication
// access to a restricted resource for a limited time without needing a Google
// account or signing in.
// For more information about signed URLs, see "[Overview of access control]."
//
// This method only requires the Method and Expires fields in the specified
// SignedURLOptions opts to be non-nil. If not provided, it attempts to fill the
// GoogleAccessID and PrivateKey from the GOOGLE_APPLICATION_CREDENTIALS environment variable.
// If you are authenticating with a custom HTTP client, Service Account based
// auto-detection will be hindered.
// This method requires the Method and Expires fields in the specified
// SignedURLOptions to be non-nil.
//
// If no private key is found, it attempts to use the GoogleAccessID to sign the URL.
// This requires the IAM Service Account Credentials API to be enabled
// (https://console.developers.google.com/apis/api/iamcredentials.googleapis.com/overview)
// and iam.serviceAccounts.signBlob permissions on the GoogleAccessID service account.
// If you do not want these fields set for you, you may pass them in through opts or use
// SignedURL(bucket, name string, opts *SignedURLOptions) instead.
// If the GoogleAccessID and PrivateKey fields are not provided, they will be
// automatically detected when:
BrennaEpp marked this conversation as resolved.
Show resolved Hide resolved
// - you are authenticated to the Storage Client with a service account's
// downloaded private key, either directly in code or by setting the
// GOOGLE_APPLICATION_CREDENTIALS environment variable (see [Other Environments]),
// - your application is running on Google Compute Engine (GCE), or
// - you are logged into [gcloud using application default credentials]
// with [impersonation enabled].
// In some cases, you may not need to set PrivateKey but must set GoogleAccessID.
tritone marked this conversation as resolved.
Show resolved Hide resolved
// GoogleAccessID should be set to a service account that will be used to attempt
// to sign the URL. This is true of cases where credentials are provided but not
// attached to a service account, such as when:
// - you are authenticated to the Storage Client with a token source,
// - you are using a custom HTTP client, or
// - you are logged into [gcloud using application default credentials]
// without [impersonation enabled].
// To sign the URL with only the GoogleAccessID set you require:
// - the [IAM Service Account Credentials API enabled], and
BrennaEpp marked this conversation as resolved.
Show resolved Hide resolved
// - iam.serviceAccounts.signBlob permissions on the GoogleAccessID service account.

// [Overview of access control]: https://cloud.google.com/storage/docs/accesscontrol#signed_urls_query_string_authentication
// [Other Environments]: https://cloud.google.com/storage/docs/authentication#libauth
// [gcloud using application default credentials]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
// [impersonation enabled]: https://cloud.google.com/sdk/gcloud/reference#--impersonate-service-account
// [IAM Service Account Credentials API enabled]: https://console.developers.google.com/apis/api/iamcredentials.googleapis.com/overview
func (b *BucketHandle) SignedURL(object string, opts *SignedURLOptions) (string, error) {
if opts.GoogleAccessID != "" && (opts.SignBytes != nil || len(opts.PrivateKey) > 0) {
return SignedURL(b.name, object, opts)
Expand Down Expand Up @@ -212,18 +229,34 @@ func (b *BucketHandle) SignedURL(object string, opts *SignedURLOptions) (string,
// GenerateSignedPostPolicyV4 generates a PostPolicyV4 value from bucket, object and opts.
// The generated URL and fields will then allow an unauthenticated client to perform multipart uploads.
//
// This method only requires the Expires field in the specified PostPolicyV4Options
// to be non-nil. If not provided, it attempts to fill the GoogleAccessID and PrivateKey
// from the GOOGLE_APPLICATION_CREDENTIALS environment variable.
// If you are authenticating with a custom HTTP client, Service Account based
// auto-detection will be hindered.
// This method requires the Expires field in the specified PostPolicyV4Options
// to be non-nil.
//
// If no private key is found, it attempts to use the GoogleAccessID to sign the URL.
// This requires the IAM Service Account Credentials API to be enabled
// (https://console.developers.google.com/apis/api/iamcredentials.googleapis.com/overview)
// and iam.serviceAccounts.signBlob permissions on the GoogleAccessID service account.
// If you do not want these fields set for you, you may pass them in through opts or use
// GenerateSignedPostPolicyV4(bucket, name string, opts *PostPolicyV4Options) instead.
// If the GoogleAccessID and PrivateKey fields are not provided, they will be
tritone marked this conversation as resolved.
Show resolved Hide resolved
// automatically detected when:
// - you are authenticated to the Storage Client with a service account's
// downloaded private key, either directly in code or by setting the
// GOOGLE_APPLICATION_CREDENTIALS environment variable (see [Other Environments]),
// - your application is running on Google Compute Engine (GCE), or
// - you are logged into [gcloud using application default credentials]
// with [impersonation enabled].
// In some cases, you may not need to set PrivateKey but must set GoogleAccessID.
// GoogleAccessID should be set to a service account that will be used to attempt
// to sign the PostPolicyV4. This is true of cases where credentials are provided
// but not attached to a service account, such as when:
// - you are authenticated to the Storage Client with a token source,
// - you are using a custom HTTP client, or
// - you are logged into [gcloud using application default credentials]
// without [impersonation enabled].
// To generate the PostPolicyV4 with only the GoogleAccessID set you require:
// - the [IAM Service Account Credentials API enabled], and
// - iam.serviceAccounts.signBlob permissions on the GoogleAccessID service account.

// [Overview of access control]: https://cloud.google.com/storage/docs/accesscontrol#signed_urls_query_string_authentication
// [Other Environments]: https://cloud.google.com/storage/docs/authentication#libauth
// [gcloud using application default credentials]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
// [impersonation enabled]: https://cloud.google.com/sdk/gcloud/reference#--impersonate-service-account
// [IAM Service Account Credentials API enabled]: https://console.developers.google.com/apis/api/iamcredentials.googleapis.com/overview
func (b *BucketHandle) GenerateSignedPostPolicyV4(object string, opts *PostPolicyV4Options) (*PostPolicyV4, error) {
if opts.GoogleAccessID != "" && (opts.SignRawBytes != nil || opts.SignBytes != nil || len(opts.PrivateKey) > 0) {
return GenerateSignedPostPolicyV4(b.name, object, opts)
Expand Down Expand Up @@ -263,17 +296,27 @@ func (b *BucketHandle) detectDefaultGoogleAccessID() (string, error) {

if b.c.creds != nil && len(b.c.creds.JSON) > 0 {
var sa struct {
ClientEmail string `json:"client_email"`
ClientEmail string `json:"client_email"`
SAImpersonationURL string `json:"service_account_impersonation_url"`
CredType string `json:"type"`
}

err := json.Unmarshal(b.c.creds.JSON, &sa)
if err == nil && sa.ClientEmail != "" {
return sa.ClientEmail, nil
} else if err != nil {
if err != nil {
returnErr = err
} else if sa.CredType == "impersonated_service_account" {
start, end := strings.LastIndex(sa.SAImpersonationURL, "/"), strings.LastIndex(sa.SAImpersonationURL, ":")

if end <= start {
returnErr = errors.New("error parsing impersonated service account credentials")
} else {
return sa.SAImpersonationURL[start+1 : end], nil
}
} else if sa.CredType == "service_account" && sa.ClientEmail != "" {
return sa.ClientEmail, nil
} else {
returnErr = errors.New("storage: empty client email in credentials")
returnErr = errors.New("unable to parse credentials")
tritone marked this conversation as resolved.
Show resolved Hide resolved
}

}

// Don't error out if we can't unmarshal, fallback to GCE check.
Expand All @@ -284,7 +327,7 @@ func (b *BucketHandle) detectDefaultGoogleAccessID() (string, error) {
} else if err != nil {
returnErr = err
} else {
returnErr = errors.New("got empty email from GCE metadata service")
returnErr = errors.New("empty email from GCE metadata service")
}

}
Expand Down
88 changes: 88 additions & 0 deletions storage/bucket_test.go
Expand Up @@ -15,12 +15,14 @@
package storage

import (
"fmt"
"testing"
"time"

"cloud.google.com/go/internal/testutil"
"github.com/google/go-cmp/cmp"
gax "github.com/googleapis/gax-go/v2"
"golang.org/x/oauth2/google"
"google.golang.org/api/googleapi"
raw "google.golang.org/api/storage/v1"
)
Expand Down Expand Up @@ -782,3 +784,89 @@ func TestBucketRetryer(t *testing.T) {
})
}
}

func TestDetectDefaultGoogleAccessID(t *testing.T) {
testCases := []struct {
name string
serviceAccount string
creds func(string) string
expectSuccess bool
}{
{
name: "impersonated creds",
serviceAccount: "default@my-project.iam.gserviceaccount.com",
creds: func(sa string) string {
return fmt.Sprintf(`{
"delegates": [],
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken",
"source_credentials": {
"client_id": "id",
"client_secret": "secret",
"refresh_token": "token",
"type": "authorized_user"
},
"type": "impersonated_service_account"
}`, sa)
},
expectSuccess: true,
},
{
name: "gcloud ADC creds",
serviceAccount: "default@my-project.iam.gserviceaccount.com",
creds: func(sa string) string {
return fmt.Sprint(`{
"client_id": "my-id.apps.googleusercontent.com",
"client_secret": "secret",
"quota_project_id": "",
"refresh_token": "token",
"type": "authorized_user"
}`)
},
expectSuccess: false,
},
{
name: "ADC private key",
serviceAccount: "default@my-project.iam.gserviceaccount.com",
creds: func(sa string) string {
return fmt.Sprintf(`{
"type": "service_account",
"project_id": "my-project",
"private_key_id": "my1",
"private_key": "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----\n",
"client_email": "%s",
"client_id": "01",
"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": "cert"
}`, sa)
},
expectSuccess: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
bucket := BucketHandle{
c: &Client{
creds: &google.Credentials{
JSON: []byte(tc.creds(tc.serviceAccount)),
},
},
name: "my-bucket",
}

id, err := bucket.detectDefaultGoogleAccessID()
if tc.expectSuccess {
if err != nil {
t.Fatal(err)
}
if id != tc.serviceAccount {
t.Errorf("service account not found correctly; got: %s, want: %s", id, tc.serviceAccount)
}
} else if err == nil {
t.Error("expected error but detectDefaultGoogleAccessID did not return one")
}
})
}
}