From 105dabb47a35ec3fc5f6bf80f6923109cbc02148 Mon Sep 17 00:00:00 2001 From: bhagya <43932219+bhagya05@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:47:22 +0530 Subject: [PATCH] Add ability to generate signed url in gcp bucket (#3393) Signed-off-by: bhagya05 Co-authored-by: Yaron Schneider --- bindings/gcp/bucket/bucket.go | 65 +++++++++++++++++++++++++++++- bindings/gcp/bucket/bucket_test.go | 4 +- bindings/gcp/bucket/metadata.yaml | 6 +++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/bindings/gcp/bucket/bucket.go b/bindings/gcp/bucket/bucket.go index 5fc9e7dec0..7fa929ae40 100644 --- a/bindings/gcp/bucket/bucket.go +++ b/bindings/gcp/bucket/bucket.go @@ -25,6 +25,7 @@ import ( "net/url" "reflect" "strconv" + "time" "cloud.google.com/go/storage" "github.com/google/uuid" @@ -43,11 +44,13 @@ const ( objectURLBase = "https://storage.googleapis.com/%s/%s" metadataDecodeBase64 = "decodeBase64" metadataEncodeBase64 = "encodeBase64" + metadataSignTTL = "signTTL" metadataKey = "key" maxResults = 1000 metadataKeyBC = "name" + signOperation = "sign" ) // GCPStorage allows saving data to GCP bucket storage. @@ -73,6 +76,7 @@ type gcpMetadata struct { Bucket string `json:"bucket" mapstructure:"bucket"` DecodeBase64 bool `json:"decodeBase64,string" mapstructure:"decodeBase64"` EncodeBase64 bool `json:"encodeBase64,string" mapstructure:"encodeBase64"` + SignTTL string `json:"signTTL" mapstructure:"signTTL" mdignore:"true"` } type listPayload struct { @@ -80,6 +84,9 @@ type listPayload struct { MaxResults int32 `json:"maxResults"` Delimiter string `json:"delimiter"` } +type signResponse struct { + SignURL string `json:"signURL"` +} type createResponse struct { ObjectURL string `json:"objectURL"` @@ -130,6 +137,7 @@ func (g *GCPStorage) Operations() []bindings.OperationKind { bindings.GetOperation, bindings.DeleteOperation, bindings.ListOperation, + signOperation, } } @@ -145,6 +153,8 @@ func (g *GCPStorage) Invoke(ctx context.Context, req *bindings.InvokeRequest) (* return g.delete(ctx, req) case bindings.ListOperation: return g.list(ctx, req) + case signOperation: + return g.sign(ctx, req) default: return nil, fmt.Errorf("unsupported operation %s", req.Operation) } @@ -312,7 +322,9 @@ func (metadata gcpMetadata) mergeWithRequestMetadata(req *bindings.InvokeRequest if val, ok := req.Metadata[metadataEncodeBase64]; ok && val != "" { merged.EncodeBase64 = utils.IsTruthy(val) } - + if val, ok := req.Metadata[metadataSignTTL]; ok && val != "" { + merged.SignTTL = val + } return merged, nil } @@ -332,3 +344,54 @@ func (g *GCPStorage) GetComponentMetadata() (metadataInfo metadata.MetadataMap) metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType) return } + +func (g *GCPStorage) sign(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { + metadata, err := g.metadata.mergeWithRequestMetadata(req) + if err != nil { + return nil, fmt.Errorf("gcp binding error. error merge metadata : %w", err) + } + + var key string + if val, ok := req.Metadata[metadataKey]; ok && val != "" { + key = val + } else { + return nil, fmt.Errorf("gcp bucket binding error: can't read key value") + } + + if metadata.SignTTL == "" { + return nil, fmt.Errorf("gcp bucket binding error: required metadata '%s' missing", metadataSignTTL) + } + + signURL, err := g.signObject(metadata.Bucket, key, metadata.SignTTL) + if err != nil { + return nil, fmt.Errorf("gcp bucket binding error: %w", err) + } + + jsonResponse, err := json.Marshal(signResponse{ + SignURL: signURL, + }) + if err != nil { + return nil, fmt.Errorf("gcp bucket binding error: error marshalling sign response: %w", err) + } + return &bindings.InvokeResponse{ + Data: jsonResponse, + }, nil +} + +func (g *GCPStorage) signObject(bucket, object, ttl string) (string, error) { + d, err := time.ParseDuration(ttl) + if err != nil { + return "", fmt.Errorf("gcp bucket binding error: error parsing signTTL: %w", err) + } + opts := &storage.SignedURLOptions{ + Scheme: storage.SigningSchemeV4, + Method: "GET", + Expires: time.Now().Add(d), + } + + u, err := g.client.Bucket(g.metadata.Bucket).SignedURL(object, opts) + if err != nil { + return "", fmt.Errorf("Bucket(%q).SignedURL: %w", bucket, err) + } + return u, nil +} diff --git a/bindings/gcp/bucket/bucket_test.go b/bindings/gcp/bucket/bucket_test.go index 20fb87a9a2..6922050acb 100644 --- a/bindings/gcp/bucket/bucket_test.go +++ b/bindings/gcp/bucket/bucket_test.go @@ -40,6 +40,7 @@ func TestParseMetadata(t *testing.T) { "projectID": "my_project_id", "tokenURI": "my_token_uri", "type": "my_type", + "signTTL": "15s", } gs := GCPStorage{logger: logger.NewLogger("test")} meta, err := gs.parseMetadata(m) @@ -57,6 +58,7 @@ func TestParseMetadata(t *testing.T) { assert.Equal(t, "my_project_id", meta.ProjectID) assert.Equal(t, "my_token_uri", meta.TokenURI) assert.Equal(t, "my_type", meta.Type) + assert.Equal(t, "15s", meta.SignTTL) }) t.Run("Metadata is correctly marshalled to JSON", func(t *testing.T) { @@ -67,7 +69,7 @@ func TestParseMetadata(t *testing.T) { "\"private_key\":\"my_private_key\",\"client_email\":\"my_email@mail.dapr\",\"client_id\":\"my_client_id\","+ "\"auth_uri\":\"my_auth_uri\",\"token_uri\":\"my_token_uri\",\"auth_provider_x509_cert_url\":\"my_auth_provider_x509\","+ "\"client_x509_cert_url\":\"my_client_x509\",\"bucket\":\"my_bucket\",\"decodeBase64\":\"false\","+ - "\"encodeBase64\":\"false\"}", string(json)) + "\"encodeBase64\":\"false\",\"signTTL\":\"15s\"}", string(json)) }) }) diff --git a/bindings/gcp/bucket/metadata.yaml b/bindings/gcp/bucket/metadata.yaml index e45a072a21..6e3448744e 100644 --- a/bindings/gcp/bucket/metadata.yaml +++ b/bindings/gcp/bucket/metadata.yaml @@ -23,6 +23,12 @@ metadata: The bucket name. example: '"mybucket"' type: string + - name: signTTL + required: false + description: | + Specifies the duration that the signed URL should be valid. + example: '"15m, 1h"' + type: string - name: decodeBase64 type: bool required: false