diff --git a/storage/bucket.go b/storage/bucket.go index 7b1757b83de..7208f577e66 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -244,6 +244,13 @@ type BucketAttrs struct { // for more information. UniformBucketLevelAccess UniformBucketLevelAccess + // PublicAccessPrevention is the setting for the bucket's + // PublicAccessPrevention policy, which can be used to prevent public access + // of data in the bucket. See + // https://cloud.google.com/storage/docs/public-access-prevention for more + // information. + PublicAccessPrevention PublicAccessPrevention + // DefaultObjectACL is the list of access controls to // apply to new objects when no object ACL is provided. DefaultObjectACL []ACLRule @@ -353,6 +360,41 @@ type UniformBucketLevelAccess struct { LockedTime time.Time } +// PublicAccessPrevention configures the Public Access Prevention feature, which +// can be used to disallow public access to any data in a bucket. See +// https://cloud.google.com/storage/docs/public-access-prevention for more +// information. +type PublicAccessPrevention int + +const ( + // PublicAccessPreventionUnknown is a zero value, used only if this field is + // not set in a call to GCS. + PublicAccessPreventionUnknown PublicAccessPrevention = iota + + // PublicAccessPreventionUnspecified corresponds to a value of "unspecified" + // and is the default for buckets. + PublicAccessPreventionUnspecified + + // PublicAccessPreventionEnforced corresponds to a value of "enforced". This + // enforces Public Access Prevention on the bucket. + PublicAccessPreventionEnforced + + publicAccessPreventionUnknown string = "" + publicAccessPreventionUnspecified = "unspecified" + publicAccessPreventionEnforced = "enforced" +) + +func (p PublicAccessPrevention) String() string { + switch p { + case PublicAccessPreventionUnspecified: + return publicAccessPreventionUnspecified + case PublicAccessPreventionEnforced: + return publicAccessPreventionEnforced + default: + return publicAccessPreventionUnknown + } +} + // Lifecycle is the lifecycle configuration for objects in the bucket. type Lifecycle struct { Rules []LifecycleRule @@ -551,6 +593,7 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) { Website: toBucketWebsite(b.Website), BucketPolicyOnly: toBucketPolicyOnly(b.IamConfiguration), UniformBucketLevelAccess: toUniformBucketLevelAccess(b.IamConfiguration), + PublicAccessPrevention: toPublicAccessPrevention(b.IamConfiguration), Etag: b.Etag, LocationType: b.LocationType, }, nil @@ -578,11 +621,15 @@ func (b *BucketAttrs) toRawBucket() *raw.Bucket { bb = &raw.BucketBilling{RequesterPays: true} } var bktIAM *raw.BucketIamConfiguration - if b.UniformBucketLevelAccess.Enabled || b.BucketPolicyOnly.Enabled { - bktIAM = &raw.BucketIamConfiguration{ - UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{ + if b.UniformBucketLevelAccess.Enabled || b.BucketPolicyOnly.Enabled || b.PublicAccessPrevention != PublicAccessPreventionUnknown { + bktIAM = &raw.BucketIamConfiguration{} + if b.UniformBucketLevelAccess.Enabled || b.BucketPolicyOnly.Enabled { + bktIAM.UniformBucketLevelAccess = &raw.BucketIamConfigurationUniformBucketLevelAccess{ Enabled: true, - }, + } + } + if b.PublicAccessPrevention != PublicAccessPreventionUnknown { + bktIAM.PublicAccessPrevention = b.PublicAccessPrevention.String() } } return &raw.Bucket{ @@ -661,6 +708,13 @@ type BucketAttrsToUpdate struct { // for more information. UniformBucketLevelAccess *UniformBucketLevelAccess + // PublicAccessPrevention is the setting for the bucket's + // PublicAccessPrevention policy, which can be used to prevent public access + // of data in the bucket. See + // https://cloud.google.com/storage/docs/public-access-prevention for more + // information. + PublicAccessPrevention PublicAccessPrevention + // StorageClass is the default storage class of the bucket. This defines // how objects in the bucket are stored and determines the SLA // and the cost of storage. Typical values are "STANDARD", "NEARLINE", @@ -771,6 +825,12 @@ func (ua *BucketAttrsToUpdate) toRawBucket() *raw.Bucket { }, } } + if ua.PublicAccessPrevention != PublicAccessPreventionUnknown { + if rb.IamConfiguration == nil { + rb.IamConfiguration = &raw.BucketIamConfiguration{} + } + rb.IamConfiguration.PublicAccessPrevention = ua.PublicAccessPrevention.String() + } if ua.Encryption != nil { if ua.Encryption.DefaultKMSKeyName == "" { rb.NullFields = append(rb.NullFields, "Encryption") @@ -1139,6 +1199,20 @@ func toUniformBucketLevelAccess(b *raw.BucketIamConfiguration) UniformBucketLeve } } +func toPublicAccessPrevention(b *raw.BucketIamConfiguration) PublicAccessPrevention { + if b == nil { + return PublicAccessPreventionUnknown + } + switch b.PublicAccessPrevention { + case publicAccessPreventionUnspecified: + return PublicAccessPreventionUnspecified + case publicAccessPreventionEnforced: + return PublicAccessPreventionEnforced + default: + return PublicAccessPreventionUnknown + } +} + // Objects returns an iterator over the objects in the bucket that match the // Query q. If q is nil, no filtering is done. Objects will be iterated over // lexicographically by name. diff --git a/storage/bucket_test.go b/storage/bucket_test.go index 123e319a84e..4ff5f5cc5ed 100644 --- a/storage/bucket_test.go +++ b/storage/bucket_test.go @@ -42,6 +42,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) { }, BucketPolicyOnly: BucketPolicyOnly{Enabled: true}, UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true}, + PublicAccessPrevention: PublicAccessPreventionEnforced, VersioningEnabled: false, // should be ignored: MetaGeneration: 39, @@ -121,6 +122,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) { UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{ Enabled: true, }, + PublicAccessPrevention: "enforced", }, Versioning: nil, // ignore VersioningEnabled if false Labels: map[string]string{"label": "value"}, @@ -205,6 +207,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) { UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{ Enabled: true, }, + PublicAccessPrevention: "enforced", } if msg := testutil.Diff(got, want); msg != "" { t.Errorf(msg) @@ -219,6 +222,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) { UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{ Enabled: true, }, + PublicAccessPrevention: "enforced", } if msg := testutil.Diff(got, want); msg != "" { t.Errorf(msg) @@ -234,6 +238,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) { UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{ Enabled: true, }, + PublicAccessPrevention: "enforced", } if msg := testutil.Diff(got, want); msg != "" { t.Errorf(msg) @@ -244,6 +249,42 @@ func TestBucketAttrsToRawBucket(t *testing.T) { attrs.BucketPolicyOnly = BucketPolicyOnly{} attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{} got = attrs.toRawBucket() + want.IamConfiguration = &raw.BucketIamConfiguration{ + PublicAccessPrevention: "enforced", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Test that setting PublicAccessPrevention to "unspecified" leads to the + // setting being propagated in the proto. + attrs.PublicAccessPrevention = PublicAccessPreventionUnspecified + got = attrs.toRawBucket() + want.IamConfiguration = &raw.BucketIamConfiguration{ + PublicAccessPrevention: "unspecified", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Re-enable UBLA and confirm that it does not affect the PAP setting. + attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true} + got = attrs.toRawBucket() + want.IamConfiguration = &raw.BucketIamConfiguration{ + UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{ + Enabled: true, + }, + PublicAccessPrevention: "unspecified", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Disable UBLA and reset PAP to default. Confirm that the IAM config is set + // to nil in the proto. + attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: false} + attrs.PublicAccessPrevention = PublicAccessPreventionUnknown + got = attrs.toRawBucket() want.IamConfiguration = nil if msg := testutil.Diff(got, want); msg != "" { t.Errorf(msg) diff --git a/storage/integration_test.go b/storage/integration_test.go index 16fd736f486..043cc5a8bd5 100644 --- a/storage/integration_test.go +++ b/storage/integration_test.go @@ -52,6 +52,7 @@ import ( "google.golang.org/api/iterator" itesting "google.golang.org/api/iterator/testing" "google.golang.org/api/option" + iampb "google.golang.org/genproto/googleapis/iam/v1" ) const ( @@ -575,6 +576,87 @@ func TestIntegration_UniformBucketLevelAccess(t *testing.T) { } } +func TestIntegration_PublicAccessPrevention(t *testing.T) { + ctx := context.Background() + client := testConfig(ctx, t) + defer client.Close() + h := testHelper{t} + + // Create a bucket with PublicAccessPrevention enforced. + bkt := client.Bucket(uidSpace.New()) + h.mustCreate(bkt, testutil.ProjID(), &BucketAttrs{PublicAccessPrevention: PublicAccessPreventionEnforced}) + defer h.mustDeleteBucket(bkt) + + // Making bucket public should fail. + policy, err := bkt.IAM().V3().Policy(ctx) + if err != nil { + t.Fatalf("fetching bucket IAM policy: %v", err) + } + policy.Bindings = append(policy.Bindings, &iampb.Binding{ + Role: "roles/storage.objectViewer", + Members: []string{iam.AllUsers}, + }) + if err := bkt.IAM().V3().SetPolicy(ctx, policy); err == nil { + t.Error("SetPolicy: expected adding AllUsers policy to bucket should fail") + } + + // Making object public via ACL should fail. + o := bkt.Object("publicAccessPrevention") + defer func() { + if err := o.Delete(ctx); err != nil { + log.Printf("failed to delete test object: %v", err) + } + }() + wc := o.NewWriter(ctx) + wc.ContentType = "text/plain" + h.mustWrite(wc, []byte("test")) + a := o.ACL() + if err := a.Set(ctx, AllUsers, RoleReader); err == nil { + t.Error("ACL.Set: expected adding AllUsers ACL to object should fail") + } + + // Update PAP setting to unspecified should work and not affect UBLA setting. + attrs, err := bkt.Update(ctx, BucketAttrsToUpdate{PublicAccessPrevention: PublicAccessPreventionUnspecified}) + if err != nil { + t.Fatalf("updating PublicAccessPrevention failed: %v", err) + } + if attrs.PublicAccessPrevention != PublicAccessPreventionUnspecified { + t.Errorf("updating PublicAccessPrevention: got %s, want %s", attrs.PublicAccessPrevention, PublicAccessPreventionUnspecified) + } + if attrs.UniformBucketLevelAccess.Enabled || attrs.BucketPolicyOnly.Enabled { + t.Error("updating PublicAccessPrevention changed UBLA setting") + } + + // Now, making object public or making bucket public should succeed. + a = o.ACL() + if err := a.Set(ctx, AllUsers, RoleReader); err != nil { + t.Errorf("ACL.Set: making object public failed: %v", err) + } + policy, err = bkt.IAM().V3().Policy(ctx) + if err != nil { + t.Fatalf("fetching bucket IAM policy: %v", err) + } + policy.Bindings = append(policy.Bindings, &iampb.Binding{ + Role: "roles/storage.objectViewer", + Members: []string{iam.AllUsers}, + }) + if err := bkt.IAM().V3().SetPolicy(ctx, policy); err != nil { + t.Errorf("SetPolicy: making bucket public failed: %v", err) + } + + // Updating UBLA should not affect PAP setting. + attrs, err = bkt.Update(ctx, BucketAttrsToUpdate{UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: true}}) + if err != nil { + t.Fatalf("updating UBLA failed: %v", err) + } + if !attrs.UniformBucketLevelAccess.Enabled { + t.Error("updating UBLA: got UBLA not enabled, want enabled") + } + if attrs.PublicAccessPrevention != PublicAccessPreventionUnspecified { + t.Errorf("updating UBLA: got %s, want %s", attrs.PublicAccessPrevention, PublicAccessPreventionUnspecified) + } +} + func TestIntegration_ConditionalDelete(t *testing.T) { ctx := context.Background() client := testConfig(ctx, t)