Skip to content

Commit

Permalink
feat(storage): GenerateSignedPostPolicyV4 can use existing creds to a…
Browse files Browse the repository at this point in the history
…uthenticate (#5105)
  • Loading branch information
BrennaEpp committed Dec 1, 2021
1 parent 014b314 commit 46489f4
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 5 deletions.
50 changes: 48 additions & 2 deletions storage/bucket.go
Expand Up @@ -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)")
Expand Down
78 changes: 75 additions & 3 deletions storage/integration_test.go
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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)

Expand Down Expand Up @@ -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)
}
}

Expand Down
15 changes: 15 additions & 0 deletions storage/post_policy_v4.go
Expand Up @@ -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.
Expand Down

0 comments on commit 46489f4

Please sign in to comment.