diff --git a/storage/bucket.go b/storage/bucket.go index 0d3be39eebc..e61ac922f79 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -244,6 +244,8 @@ type BucketAttrs struct { // for more information. UniformBucketLevelAccess UniformBucketLevelAccess + PublicAccessPrevention PublicAccessPrevention + // DefaultObjectACL is the list of access controls to // apply to new objects when no object ACL is provided. DefaultObjectACL []ACLRule @@ -353,6 +355,29 @@ type UniformBucketLevelAccess struct { LockedTime time.Time } +type PublicAccessPrevention int + +const ( + PublicAccessPreventionDefault PublicAccessPrevention = iota + PublicAccessPreventionUnspecified + PublicAccessPreventionEnforced + + publicAccessPreventionDefault string = "" + publicAccessPreventionUnspecified = "unspecified" + publicAccessPreventionEnforced = "enforced" +) + +func (p PublicAccessPrevention) string() string { + switch p { + case PublicAccessPreventionUnspecified: + return publicAccessPreventionUnspecified + case PublicAccessPreventionEnforced: + return publicAccessPreventionEnforced + default: + return publicAccessPreventionDefault + } +} + // Lifecycle is the lifecycle configuration for objects in the bucket. type Lifecycle struct { Rules []LifecycleRule @@ -551,6 +576,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 +604,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 != PublicAccessPreventionDefault { + bktIAM = &raw.BucketIamConfiguration{} + if b.UniformBucketLevelAccess.Enabled || b.BucketPolicyOnly.Enabled { + bktIAM.UniformBucketLevelAccess = &raw.BucketIamConfigurationUniformBucketLevelAccess{ Enabled: true, - }, + } + } + if b.PublicAccessPrevention != PublicAccessPreventionDefault { + bktIAM.PublicAccessPrevention = b.PublicAccessPrevention.string() } } return &raw.Bucket{ @@ -661,6 +691,8 @@ type BucketAttrsToUpdate struct { // for more information. UniformBucketLevelAccess *UniformBucketLevelAccess + 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 +803,12 @@ func (ua *BucketAttrsToUpdate) toRawBucket() *raw.Bucket { }, } } + if ua.PublicAccessPrevention != PublicAccessPreventionDefault { + 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 +1177,20 @@ func toUniformBucketLevelAccess(b *raw.BucketIamConfiguration) UniformBucketLeve } } +func toPublicAccessPrevention(b *raw.BucketIamConfiguration) PublicAccessPrevention { + if b == nil { + return PublicAccessPreventionDefault + } + switch b.PublicAccessPrevention { + case publicAccessPreventionUnspecified: + return PublicAccessPreventionUnspecified + case publicAccessPreventionEnforced: + return PublicAccessPreventionEnforced + default: + return PublicAccessPreventionDefault + } +} + // 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..234f8c89b1d 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,10 +249,47 @@ 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 = PublicAccessPreventionDefault + got = attrs.toRawBucket() want.IamConfiguration = nil if msg := testutil.Diff(got, want); msg != "" { t.Errorf(msg) } + } func TestBucketAttrsToUpdateToRawBucket(t *testing.T) { diff --git a/storage/integration_test.go b/storage/integration_test.go index c21ff8430a0..3db3f9b6ad2 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,88 @@ 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 %v, want %v", attrs.PublicAccessPrevention.string(), PublicAccessPreventionUnspecified.string()) + } + 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 %v, want %v", attrs.PublicAccessPrevention.string(), PublicAccessPreventionUnspecified.string()) + } + +} + func TestIntegration_ConditionalDelete(t *testing.T) { ctx := context.Background() client := testConfig(ctx, t)