From 46489f4c8a634068a3e7cf2fd5e5ca11b555c0a8 Mon Sep 17 00:00:00 2001 From: Brenna N Epp Date: Wed, 1 Dec 2021 21:51:46 +0000 Subject: [PATCH] feat(storage): GenerateSignedPostPolicyV4 can use existing creds to authenticate (#5105) --- storage/bucket.go | 50 +++++++++++++++++++++++- storage/integration_test.go | 78 +++++++++++++++++++++++++++++++++++-- storage/post_policy_v4.go | 15 +++++++ 3 files changed, 138 insertions(+), 5 deletions(-) diff --git a/storage/bucket.go b/storage/bucket.go index ec7dcb5c322..93221c5a75c 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -282,8 +282,54 @@ func (b *BucketHandle) SignedURL(object string, opts *SignedURLOptions) (string, return SignedURL(b.name, object, newopts) } -// TODO: Add a similar wrapper for GenerateSignedPostPolicyV4 allowing users to -// omit PrivateKey/SignBytes +// 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. +// +// 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. +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) + } + // Make a copy of opts so we don't modify the pointer parameter. + newopts := opts.clone() + + if newopts.GoogleAccessID == "" { + id, err := b.detectDefaultGoogleAccessID() + if err != nil { + return nil, err + } + newopts.GoogleAccessID = id + } + if newopts.SignBytes == nil && newopts.SignRawBytes == nil && len(newopts.PrivateKey) == 0 { + if b.c.creds != nil && len(b.c.creds.JSON) > 0 { + var sa struct { + PrivateKey string `json:"private_key"` + } + err := json.Unmarshal(b.c.creds.JSON, &sa) + if err == nil && sa.PrivateKey != "" { + newopts.PrivateKey = []byte(sa.PrivateKey) + } + } + + // Don't error out if we can't unmarshal the private key from the client, + // fallback to the default sign function for the service account. + if len(newopts.PrivateKey) == 0 { + newopts.SignRawBytes = b.defaultSignBytesFunc(newopts.GoogleAccessID) + } + } + return GenerateSignedPostPolicyV4(b.name, object, newopts) +} func (b *BucketHandle) detectDefaultGoogleAccessID() (string, error) { returnErr := errors.New("no credentials found on client and not on GCE (Google Compute Engine)") diff --git a/storage/integration_test.go b/storage/integration_test.go index 6d3567d0c2a..b0ebf509643 100644 --- a/storage/integration_test.go +++ b/storage/integration_test.go @@ -4142,7 +4142,7 @@ func TestIntegration_PostPolicyV4(t *testing.T) { object := b.Object(objectName) defer h.mustDeleteObject(object) - pv4, err := GenerateSignedPostPolicyV4(newBucketName, objectName, opts) + pv4, err := b.GenerateSignedPostPolicyV4(objectName, opts) if err != nil { t.Fatal(err) } @@ -4248,6 +4248,78 @@ func TestIntegration_SignedURL_Bucket(t *testing.T) { } } +func TestIntegration_PostPolicyV4_Bucket(t *testing.T) { + h := testHelper{t} + ctx := context.Background() + + if testing.Short() && !replaying { + t.Skip("Integration tests skipped in short mode") + } + + // We explictly send the key to the client to sign with the private key + clientWithCredentials := newTestClientWithExplicitCredentials(ctx, t) + defer clientWithCredentials.Close() + + // Create another client to test the sign byte function as well + clientWithoutPrivateKey := testConfig(ctx, t, ScopeFullControl, "https://www.googleapis.com/auth/cloud-platform") + defer clientWithoutPrivateKey.Close() + + jwt, err := testutil.JWTConfig() + if err != nil { + t.Fatalf("unable to find test credentials: %v", err) + } + + statusCodeToRespond := 200 + + for _, test := range []struct { + desc string + opts PostPolicyV4Options + client *Client + }{ + { + desc: "signing with the private key", + opts: PostPolicyV4Options{ + Expires: time.Now().Add(30 * time.Minute), + + Fields: &PolicyV4Fields{ + StatusCodeOnSuccess: statusCodeToRespond, + ContentType: "text/plain", + ACL: "public-read", + }, + }, + client: clientWithCredentials, + }, + { + desc: "signing with the default sign bytes func", + opts: PostPolicyV4Options{ + Expires: time.Now().Add(30 * time.Minute), + GoogleAccessID: jwt.Email, + Fields: &PolicyV4Fields{ + StatusCodeOnSuccess: statusCodeToRespond, + ContentType: "text/plain", + ACL: "public-read", + }, + }, + client: clientWithoutPrivateKey, + }, + } { + t.Run(test.desc, func(t *testing.T) { + objectName := uidSpace.New() + object := test.client.Bucket(bucketName).Object(objectName) + defer h.mustDeleteObject(object) + + pv4, err := test.client.Bucket(bucketName).GenerateSignedPostPolicyV4(objectName, &test.opts) + if err != nil { + t.Fatal(err) + } + + if err := verifyPostPolicy(pv4, object, bytes.Repeat([]byte("a"), 25), statusCodeToRespond); err != nil { + t.Fatal(err) + } + }) + } +} + // Tests that the same SignBytes function works for both // SignRawBytes on GeneratePostPolicyV4 and SignBytes on SignedURL func TestIntegration_PostPolicyV4_SignedURL_WithSignBytes(t *testing.T) { @@ -4263,7 +4335,7 @@ func TestIntegration_PostPolicyV4_SignedURL_WithSignBytes(t *testing.T) { h := testHelper{t} projectID := testutil.ProjID() bucketName := uidSpace.New() - objectName := "my-object.txt" + objectName := uidSpace.New() fileBody := bytes.Repeat([]byte("b"), 25) bucket := client.Bucket(bucketName) @@ -4491,7 +4563,7 @@ func (h testHelper) mustObjectAttrs(o *ObjectHandle) *ObjectAttrs { func (h testHelper) mustDeleteObject(o *ObjectHandle) { if err := o.Delete(context.Background()); err != nil { - h.t.Fatalf("%s: object delete: %v", loc(), err) + h.t.Fatalf("%s: delete object %s from bucket %s: %v", loc(), o.ObjectName(), o.BucketName(), err) } } diff --git a/storage/post_policy_v4.go b/storage/post_policy_v4.go index 5f418c3246b..7e972101015 100644 --- a/storage/post_policy_v4.go +++ b/storage/post_policy_v4.go @@ -116,6 +116,21 @@ type PostPolicyV4Options struct { shouldHashSignBytes bool } +func (opts *PostPolicyV4Options) clone() *PostPolicyV4Options { + return &PostPolicyV4Options{ + GoogleAccessID: opts.GoogleAccessID, + PrivateKey: opts.PrivateKey, + SignBytes: opts.SignBytes, + SignRawBytes: opts.SignRawBytes, + Expires: opts.Expires, + Style: opts.Style, + Insecure: opts.Insecure, + Fields: opts.Fields, + Conditions: opts.Conditions, + shouldHashSignBytes: opts.shouldHashSignBytes, + } +} + // PolicyV4Fields describes the attributes for a PostPolicyV4 request. type PolicyV4Fields struct { // ACL specifies the access control permissions for the object.