feat(storage): support PublicAccessPrevention (#3608)

This is a new field in the IAM configuration for buckets.
Support viewing/setting via bucket attrs, and add an integration
test for the feature.

Closes #3203
diff --git a/storage/bucket.go b/storage/bucket.go
index 7b1757b..7208f57 100644
--- a/storage/bucket.go
+++ b/storage/bucket.go
@@ -244,6 +244,13 @@
 	// 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 @@
 	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 @@
 		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 @@
 		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 @@
 	// 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 @@
 			},
 		}
 	}
+	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 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 123e319..4ff5f5c 100644
--- a/storage/bucket_test.go
+++ b/storage/bucket_test.go
@@ -42,6 +42,7 @@
 		},
 		BucketPolicyOnly:         BucketPolicyOnly{Enabled: true},
 		UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true},
+		PublicAccessPrevention:   PublicAccessPreventionEnforced,
 		VersioningEnabled:        false,
 		// should be ignored:
 		MetaGeneration: 39,
@@ -121,6 +122,7 @@
 			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
 				Enabled: true,
 			},
+			PublicAccessPrevention: "enforced",
 		},
 		Versioning: nil, // ignore VersioningEnabled if false
 		Labels:     map[string]string{"label": "value"},
@@ -205,6 +207,7 @@
 		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
 			Enabled: true,
 		},
+		PublicAccessPrevention: "enforced",
 	}
 	if msg := testutil.Diff(got, want); msg != "" {
 		t.Errorf(msg)
@@ -219,6 +222,7 @@
 		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
 			Enabled: true,
 		},
+		PublicAccessPrevention: "enforced",
 	}
 	if msg := testutil.Diff(got, want); msg != "" {
 		t.Errorf(msg)
@@ -234,6 +238,7 @@
 		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
 			Enabled: true,
 		},
+		PublicAccessPrevention: "enforced",
 	}
 	if msg := testutil.Diff(got, want); msg != "" {
 		t.Errorf(msg)
@@ -244,6 +249,42 @@
 	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 16fd736..043cc5a 100644
--- a/storage/integration_test.go
+++ b/storage/integration_test.go
@@ -52,6 +52,7 @@
 	"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_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)