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): GenerateSignedPostPolicyV4 can use existing creds to authenticate #5105

Merged
merged 9 commits into from Dec 1, 2021
Merged
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
80 changes: 76 additions & 4 deletions storage/integration_test.go
Expand Up @@ -4174,7 +4174,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 @@ -4280,6 +4280,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 @@ -4295,7 +4367,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 @@ -4335,7 +4407,7 @@ func TestIntegration_PostPolicyV4_SignedURL_WithSignBytes(t *testing.T) {
},
}

pv4, err := GenerateSignedPostPolicyV4(bucketName, objectName, ppv4Opts)
pv4, err := bucket.GenerateSignedPostPolicyV4(objectName, ppv4Opts)
BrennaEpp marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -4523,7 +4595,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
14 changes: 14 additions & 0 deletions storage/post_policy_v4.go
Expand Up @@ -116,6 +116,20 @@ type PostPolicyV4Options struct {
shouldHashSignBytes bool
}

func (opts *PostPolicyV4Options) clone() *PostPolicyV4Options {
return &PostPolicyV4Options{
GoogleAccessID: opts.GoogleAccessID,
PrivateKey: opts.PrivateKey,
SignBytes: opts.SignBytes,
BrennaEpp marked this conversation as resolved.
Show resolved Hide resolved
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