diff --git a/storage/bucket.go b/storage/bucket.go index b9b6c936173..a6b139eecc2 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -945,6 +945,75 @@ func (b *BucketAttrs) toProtoBucket() *storagepb.Bucket { } } +func (ua *BucketAttrsToUpdate) toProtoBucket() *storagepb.Bucket { + if ua == nil { + return &storagepb.Bucket{} + } + + // TODO(cathyo): Handle labels. Pending b/230510191. + + var v *storagepb.Bucket_Versioning + if ua.VersioningEnabled != nil { + v = &storagepb.Bucket_Versioning{Enabled: optional.ToBool(ua.VersioningEnabled)} + } + var bb *storagepb.Bucket_Billing + if ua.RequesterPays != nil { + bb = &storage.Bucket_Billing{RequesterPays: optional.ToBool(ua.RequesterPays)} + } + var bktIAM *storagepb.Bucket_IamConfig + var ublaEnabled bool + var bktPolicyOnlyEnabled bool + if ua.UniformBucketLevelAccess != nil { + ublaEnabled = optional.ToBool(ua.UniformBucketLevelAccess.Enabled) + } + if ua.BucketPolicyOnly != nil { + bktPolicyOnlyEnabled = optional.ToBool(ua.BucketPolicyOnly.Enabled) + } + if ublaEnabled || bktPolicyOnlyEnabled { + bktIAM.UniformBucketLevelAccess = &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{ + Enabled: true, + } + } + if ua.PublicAccessPrevention != PublicAccessPreventionUnknown { + bktIAM.PublicAccessPrevention = ua.PublicAccessPrevention.String() + } + var defaultHold bool + if ua.DefaultEventBasedHold != nil { + defaultHold = optional.ToBool(ua.DefaultEventBasedHold) + } + var lifecycle Lifecycle + if ua.Lifecycle != nil { + lifecycle = *ua.Lifecycle + } + var bktACL []*storagepb.BucketAccessControl + if ua.PredefinedACL != "" { + // Clear ACL or the call will fail. + bktACL = nil + } + var bktDefaultObjectACL []*storagepb.ObjectAccessControl + if ua.PredefinedDefaultObjectACL != "" { + // Clear ACLs or the call will fail. + bktDefaultObjectACL = nil + } + + return &storagepb.Bucket{ + StorageClass: ua.StorageClass, + Acl: bktACL, + DefaultObjectAcl: bktDefaultObjectACL, + DefaultEventBasedHold: defaultHold, + Versioning: v, + Billing: bb, + Lifecycle: toProtoLifecycle(lifecycle), + RetentionPolicy: ua.RetentionPolicy.toProtoRetentionPolicy(), + Cors: toProtoCORS(ua.CORS), + Encryption: ua.Encryption.toProtoBucketEncryption(), + Logging: ua.Logging.toProtoBucketLogging(), + Website: ua.Website.toProtoBucketWebsite(), + IamConfig: bktIAM, + Rpo: ua.RPO.String(), + } +} + // CORS is the bucket's Cross-Origin Resource Sharing (CORS) configuration. type CORS struct { // MaxAge is the value to return in the Access-Control-Max-Age diff --git a/storage/client.go b/storage/client.go index b25d0efb0b9..87ee41927d0 100644 --- a/storage/client.go +++ b/storage/client.go @@ -50,7 +50,7 @@ type storageClient interface { DeleteBucket(ctx context.Context, bucket string, conds *BucketConditions, opts ...storageOption) error GetBucket(ctx context.Context, bucket string, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error) - UpdateBucket(ctx context.Context, uattrs *BucketAttrsToUpdate, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error) + UpdateBucket(ctx context.Context, bucket string, uattrs *BucketAttrsToUpdate, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error) LockBucketRetentionPolicy(ctx context.Context, bucket string, conds *BucketConditions, opts ...storageOption) error ListObjects(ctx context.Context, bucket string, q *Query, opts ...storageOption) *ObjectIterator diff --git a/storage/client_test.go b/storage/client_test.go index c0a9557447f..b86d876342d 100644 --- a/storage/client_test.go +++ b/storage/client_test.go @@ -88,6 +88,92 @@ func TestGetBucketEmulated(t *testing.T) { }) } +func TestUpdateBucketEmulated(t *testing.T) { + transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { + bkt := &BucketAttrs{ + Name: bucket, + } + // Create the bucket that will be updated. + _, err := client.CreateBucket(context.Background(), project, bkt) + if err != nil { + t.Fatalf("client.CreateBucket: %v", err) + } + + ua := &BucketAttrsToUpdate{ + VersioningEnabled: false, + RequesterPays: false, + DefaultEventBasedHold: false, + Encryption: &BucketEncryption{DefaultKMSKeyName: "key2"}, + Lifecycle: &Lifecycle{ + Rules: []LifecycleRule{ + { + Action: LifecycleAction{Type: "Delete"}, + Condition: LifecycleCondition{AgeInDays: 30}, + }, + }, + }, + Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, + Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, + StorageClass: "NEARLINE", + RPO: RPOAsyncTurbo, + } + want := &BucketAttrs{ + Name: bucket, + VersioningEnabled: false, + RequesterPays: false, + DefaultEventBasedHold: false, + Encryption: &BucketEncryption{DefaultKMSKeyName: "key2"}, + Lifecycle: Lifecycle{ + Rules: []LifecycleRule{ + { + Action: LifecycleAction{Type: "Delete"}, + Condition: LifecycleCondition{AgeInDays: 30}, + }, + }, + }, + Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, + Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, + StorageClass: "NEARLINE", + RPO: RPOAsyncTurbo, + } + + got, err := client.UpdateBucket(context.Background(), bucket, ua, &BucketConditions{MetagenerationMatch: 1}) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(got.Name, want.Name); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.VersioningEnabled, want.VersioningEnabled); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.RequesterPays, want.RequesterPays); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.DefaultEventBasedHold, want.DefaultEventBasedHold); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.Encryption, want.Encryption); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.Lifecycle, want.Lifecycle); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.Logging, want.Logging); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.Website, want.Website); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.RPO, want.RPO); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + if diff := cmp.Diff(got.StorageClass, want.StorageClass); diff != "" { + t.Errorf("got(-),want(+):\n%s", diff) + } + }) +} + func TestGetServiceAccountEmulated(t *testing.T) { transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { _, err := client.GetServiceAccount(context.Background(), project) @@ -287,6 +373,50 @@ func TestListBucketsEmulated(t *testing.T) { }) } +func TestListBucketACLsEmulated(t *testing.T) { + transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { + ctx := context.Background() + attrs := &BucketAttrs{ + Name: bucket, + PredefinedACL: "publicRead", + } + // Create the bucket that will be retrieved. + if _, err := client.CreateBucket(ctx, project, attrs); err != nil { + t.Fatalf("client.CreateBucket: %v", err) + } + + acls, err := client.ListBucketACLs(ctx, bucket) + if err != nil { + t.Fatalf("client.ListBucketACLs: %v", err) + } + if want, got := len(acls), 2; want != got { + t.Errorf("ListBucketACLs: got %v, want %v items", acls, want) + } + }) +} + +func TestListDefaultObjectACLsEmulated(t *testing.T) { + transportClientTest(t, func(t *testing.T, project, bucket string, client storageClient) { + ctx := context.Background() + attrs := &BucketAttrs{ + Name: bucket, + PredefinedDefaultObjectACL: "publicRead", + } + // Create the bucket that will be retrieved. + if _, err := client.CreateBucket(ctx, project, attrs); err != nil { + t.Fatalf("client.CreateBucket: %v", err) + } + + acls, err := client.ListDefaultObjectACLs(ctx, bucket) + if err != nil { + t.Fatalf("client.ListDefaultObjectACLs: %v", err) + } + if want, got := len(acls), 2; want != got { + t.Errorf("ListDefaultObjectACLs: got %v, want %v items", acls, want) + } + }) +} + func initEmulatorClients() func() error { noopCloser := func() error { return nil } if !isEmulatorEnvironmentSet() { diff --git a/storage/grpc_client.go b/storage/grpc_client.go index 2c7c3542c84..ef84e2ba6e7 100644 --- a/storage/grpc_client.go +++ b/storage/grpc_client.go @@ -26,6 +26,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" ) const ( @@ -255,8 +256,81 @@ func (c *grpcStorageClient) GetBucket(ctx context.Context, bucket string, conds return battrs, err } -func (c *grpcStorageClient) UpdateBucket(ctx context.Context, uattrs *BucketAttrsToUpdate, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error) { - return nil, errMethodNotSupported +func (c *grpcStorageClient) UpdateBucket(ctx context.Context, bucket string, uattrs *BucketAttrsToUpdate, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error) { + s := callSettings(c.settings, opts...) + b := uattrs.toProtoBucket() + b.Name = bucketResourceName(globalProjectAlias, bucket) + req := &storagepb.UpdateBucketRequest{ + Bucket: b, + PredefinedAcl: uattrs.PredefinedACL, + PredefinedDefaultObjectAcl: uattrs.PredefinedDefaultObjectACL, + } + if err := applyBucketCondsProto("grpcStorageClient.UpdateBucket", conds, req); err != nil { + return nil, err + } + if s.userProject != "" { + req.CommonRequestParams = &storagepb.CommonRequestParams{ + UserProject: toProjectResource(s.userProject), + } + } + + var paths []string + fieldMask := &fieldmaskpb.FieldMask{ + Paths: paths, + } + if uattrs.CORS != nil { + fieldMask.Paths = append(fieldMask.Paths, "cors") + } + if uattrs.DefaultEventBasedHold != nil { + fieldMask.Paths = append(fieldMask.Paths, "default_event_based_hold") + } + if uattrs.RetentionPolicy != nil { + fieldMask.Paths = append(fieldMask.Paths, "retention_policy") + } + if uattrs.VersioningEnabled != nil { + fieldMask.Paths = append(fieldMask.Paths, "versioning") + } + if uattrs.RequesterPays != nil { + fieldMask.Paths = append(fieldMask.Paths, "billing") + } + if uattrs.BucketPolicyOnly != nil || uattrs.UniformBucketLevelAccess != nil || uattrs.PublicAccessPrevention != PublicAccessPreventionUnknown { + fieldMask.Paths = append(fieldMask.Paths, "iam_config") + } + if uattrs.Encryption != nil { + fieldMask.Paths = append(fieldMask.Paths, "encryption") + } + if uattrs.Lifecycle != nil { + fieldMask.Paths = append(fieldMask.Paths, "lifecycle") + } + if uattrs.Logging != nil { + fieldMask.Paths = append(fieldMask.Paths, "logging") + } + if uattrs.Website != nil { + fieldMask.Paths = append(fieldMask.Paths, "website") + } + if uattrs.PredefinedACL != "" { + fieldMask.Paths = append(fieldMask.Paths, "acl") + } + if uattrs.PredefinedDefaultObjectACL != "" { + fieldMask.Paths = append(fieldMask.Paths, "default_object_acl") + } + if uattrs.StorageClass != "" { + fieldMask.Paths = append(fieldMask.Paths, "storage_class") + } + if uattrs.RPO != RPOUnknown { + fieldMask.Paths = append(fieldMask.Paths, "rpo") + } + // TODO(cathyo): Handle labels. Pending b/230510191. + req.UpdateMask = fieldMask + + var battrs *BucketAttrs + err := run(ctx, func() error { + res, err := c.raw.UpdateBucket(ctx, req, s.gax...) + battrs = newBucketFromProto(res) + return err + }, s.retry, s.idempotent) + + return battrs, err } func (c *grpcStorageClient) LockBucketRetentionPolicy(ctx context.Context, bucket string, conds *BucketConditions, opts ...storageOption) error { return errMethodNotSupported @@ -329,7 +403,11 @@ func (c *grpcStorageClient) DeleteDefaultObjectACL(ctx context.Context, bucket s return errMethodNotSupported } func (c *grpcStorageClient) ListDefaultObjectACLs(ctx context.Context, bucket string, opts ...storageOption) ([]ACLRule, error) { - return nil, errMethodNotSupported + attrs, err := c.GetBucket(ctx, bucket, nil, opts...) + if err != nil { + return nil, err + } + return attrs.DefaultObjectACL, nil } func (c *grpcStorageClient) UpdateDefaultObjectACL(ctx context.Context, opts ...storageOption) (*ACLRule, error) { return nil, errMethodNotSupported @@ -341,7 +419,11 @@ func (c *grpcStorageClient) DeleteBucketACL(ctx context.Context, bucket string, return errMethodNotSupported } func (c *grpcStorageClient) ListBucketACLs(ctx context.Context, bucket string, opts ...storageOption) ([]ACLRule, error) { - return nil, errMethodNotSupported + attrs, err := c.GetBucket(ctx, bucket, nil, opts...) + if err != nil { + return nil, err + } + return attrs.ACL, nil } func (c *grpcStorageClient) UpdateBucketACL(ctx context.Context, bucket string, entity ACLEntity, role ACLRole, opts ...storageOption) (*ACLRule, error) { return nil, errMethodNotSupported diff --git a/storage/http_client.go b/storage/http_client.go index 965bd86701a..be4e9b3c752 100644 --- a/storage/http_client.go +++ b/storage/http_client.go @@ -21,6 +21,7 @@ import ( "net/http" "net/url" "os" + "reflect" "strings" "golang.org/x/oauth2/google" @@ -270,9 +271,36 @@ func (c *httpStorageClient) GetBucket(ctx context.Context, bucket string, conds } return newBucket(resp) } -func (c *httpStorageClient) UpdateBucket(ctx context.Context, uattrs *BucketAttrsToUpdate, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error) { - return nil, errMethodNotSupported +func (c *httpStorageClient) UpdateBucket(ctx context.Context, bucket string, uattrs *BucketAttrsToUpdate, conds *BucketConditions, opts ...storageOption) (*BucketAttrs, error) { + s := callSettings(c.settings, opts...) + rb := uattrs.toRawBucket() + req := c.raw.Buckets.Patch(bucket, rb).Projection("full") + setClientHeader(req.Header()) + err := applyBucketConds("httpStorageClient.UpdateBucket", conds, req) + if err != nil { + return nil, err + } + if s.userProject != "" { + req.UserProject(s.userProject) + } + if uattrs != nil && uattrs.PredefinedACL != "" { + req.PredefinedAcl(uattrs.PredefinedACL) + } + if uattrs != nil && uattrs.PredefinedDefaultObjectACL != "" { + req.PredefinedDefaultObjectAcl(uattrs.PredefinedDefaultObjectACL) + } + + var rawBucket *raw.Bucket + err = run(ctx, func() error { + rawBucket, err = req.Context(ctx).Do() + return err + }, s.retry, s.idempotent) + if err != nil { + return nil, err + } + return newBucket(rawBucket) } + func (c *httpStorageClient) LockBucketRetentionPolicy(ctx context.Context, bucket string, conds *BucketConditions, opts ...storageOption) error { return errMethodNotSupported } @@ -354,8 +382,21 @@ func (c *httpStorageClient) UpdateObject(ctx context.Context, bucket, object str func (c *httpStorageClient) DeleteDefaultObjectACL(ctx context.Context, bucket string, entity ACLEntity, opts ...storageOption) error { return errMethodNotSupported } + func (c *httpStorageClient) ListDefaultObjectACLs(ctx context.Context, bucket string, opts ...storageOption) ([]ACLRule, error) { - return nil, errMethodNotSupported + s := callSettings(c.settings, opts...) + var acls *raw.ObjectAccessControls + var err error + err = run(ctx, func() error { + req := c.raw.DefaultObjectAccessControls.List(bucket) + configureACLCall(ctx, s.userProject, req) + acls, err = req.Do() + return err + }, s.retry, true) + if err != nil { + return nil, err + } + return toObjectACLRules(acls.Items), nil } func (c *httpStorageClient) UpdateDefaultObjectACL(ctx context.Context, opts ...storageOption) (*ACLRule, error) { return nil, errMethodNotSupported @@ -367,8 +408,32 @@ func (c *httpStorageClient) DeleteBucketACL(ctx context.Context, bucket string, return errMethodNotSupported } func (c *httpStorageClient) ListBucketACLs(ctx context.Context, bucket string, opts ...storageOption) ([]ACLRule, error) { - return nil, errMethodNotSupported + s := callSettings(c.settings, opts...) + var acls *raw.BucketAccessControls + var err error + err = run(ctx, func() error { + req := c.raw.BucketAccessControls.List(bucket) + configureACLCall(ctx, s.userProject, req) + acls, err = req.Do() + return err + }, s.retry, true) + if err != nil { + return nil, err + } + return toBucketACLRules(acls.Items), nil } + +// configureACLCall sets the context, user project and headers on the apiary library call. +// This will panic if the call does not have the correct methods. +func configureACLCall(ctx context.Context, userProject string, call interface{ Header() http.Header }) { + vc := reflect.ValueOf(call) + vc.MethodByName("Context").Call([]reflect.Value{reflect.ValueOf(ctx)}) + if userProject != "" { + vc.MethodByName("UserProject").Call([]reflect.Value{reflect.ValueOf(userProject)}) + } + setClientHeader(call.Header()) +} + func (c *httpStorageClient) UpdateBucketACL(ctx context.Context, bucket string, entity ACLEntity, role ACLRole, opts ...storageOption) (*ACLRule, error) { return nil, errMethodNotSupported }