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

Add ability to generate signed url in gcp bucket #3393

Merged
merged 7 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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