Skip to content

Commit

Permalink
Add ability to generate signed url in gcp bucket (dapr#3393)
Browse files Browse the repository at this point in the history
Signed-off-by: bhagya05 <bhagyasingh05@gmail.com>
Co-authored-by: Yaron Schneider <schneider.yaron@live.com>
  • Loading branch information
bhagya05 and yaron2 committed Jun 12, 2024
1 parent 787b23d commit 105dabb
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 2 deletions.
65 changes: 64 additions & 1 deletion bindings/gcp/bucket/bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"net/url"
"reflect"
"strconv"
"time"

"cloud.google.com/go/storage"
"github.com/google/uuid"
Expand All @@ -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.
Expand All @@ -73,13 +76,17 @@ 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 {
Prefix string `json:"prefix"`
MaxResults int32 `json:"maxResults"`
Delimiter string `json:"delimiter"`
}
type signResponse struct {
SignURL string `json:"signURL"`
}

type createResponse struct {
ObjectURL string `json:"objectURL"`
Expand Down Expand Up @@ -130,6 +137,7 @@ func (g *GCPStorage) Operations() []bindings.OperationKind {
bindings.GetOperation,
bindings.DeleteOperation,
bindings.ListOperation,
signOperation,
}
}

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

Expand All @@ -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
}
4 changes: 3 additions & 1 deletion bindings/gcp/bucket/bucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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))
})
})

Expand Down
6 changes: 6 additions & 0 deletions bindings/gcp/bucket/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 105dabb

Please sign in to comment.