storage: Add field for UniformBucketLevelAccess

The name of BucketOnlyPolicy has changed to UniformBucketLevelAccess.
The BucketOnlyPolicy field is still supported by the backend, but the
client library should prefer the new UniformBucketLevelAccess field.

Fixes #1621.

Change-Id: Ifdb346b4842676e3d1691cae8411e46f0ca284c6
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/47070
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jesse Lovelace <jesselovelace@google.com>
Reviewed-by: Frank Natividad <franknatividad@google.com>
diff --git a/storage/bucket.go b/storage/bucket.go
index fe080bf..f125f1e 100644
--- a/storage/bucket.go
+++ b/storage/bucket.go
@@ -232,10 +232,18 @@
 	// ACL is the list of access control rules on the bucket.
 	ACL []ACLRule
 
-	// BucketPolicyOnly configures access checks to use only bucket-level IAM
-	// policies.
+	// BucketPolicyOnly is an alias for UniformBucketLevelAccess. Use of
+	// UniformBucketLevelAccess is recommended above the use of this field.
+	// Setting BucketPolicyOnly.Enabled OR UniformBucketLevelAccess.Enabled to
+	// true, will enable UniformBucketLevelAccess.
 	BucketPolicyOnly BucketPolicyOnly
 
+	// UniformBucketLevelAccess configures access checks to use only bucket-level IAM
+	// policies and ignore any ACL rules for the bucket.
+	// See https://cloud.google.com/storage/docs/uniform-bucket-level-access
+	// for more information.
+	UniformBucketLevelAccess UniformBucketLevelAccess
+
 	// DefaultObjectACL is the list of access controls to
 	// apply to new objects when no object ACL is provided.
 	DefaultObjectACL []ACLRule
@@ -321,8 +329,8 @@
 	LocationType string
 }
 
-// BucketPolicyOnly configures access checks to use only bucket-level IAM
-// policies.
+// BucketPolicyOnly is an alias for UniformBucketLevelAccess.
+// Use of UniformBucketLevelAccess is preferred above BucketPolicyOnly.
 type BucketPolicyOnly struct {
 	// Enabled specifies whether access checks use only bucket-level IAM
 	// policies. Enabled may be disabled until the locked time.
@@ -332,6 +340,17 @@
 	LockedTime time.Time
 }
 
+// UniformBucketLevelAccess configures access checks to use only bucket-level IAM
+// policies.
+type UniformBucketLevelAccess struct {
+	// Enabled specifies whether access checks use only bucket-level IAM
+	// policies. Enabled may be disabled until the locked time.
+	Enabled bool
+	// LockedTime specifies the deadline for changing Enabled from true to
+	// false.
+	LockedTime time.Time
+}
+
 // Lifecycle is the lifecycle configuration for objects in the bucket.
 type Lifecycle struct {
 	Rules []LifecycleRule
@@ -488,26 +507,27 @@
 		return nil, err
 	}
 	return &BucketAttrs{
-		Name:                  b.Name,
-		Location:              b.Location,
-		MetaGeneration:        b.Metageneration,
-		DefaultEventBasedHold: b.DefaultEventBasedHold,
-		StorageClass:          b.StorageClass,
-		Created:               convertTime(b.TimeCreated),
-		VersioningEnabled:     b.Versioning != nil && b.Versioning.Enabled,
-		ACL:                   toBucketACLRules(b.Acl),
-		DefaultObjectACL:      toObjectACLRules(b.DefaultObjectAcl),
-		Labels:                b.Labels,
-		RequesterPays:         b.Billing != nil && b.Billing.RequesterPays,
-		Lifecycle:             toLifecycle(b.Lifecycle),
-		RetentionPolicy:       rp,
-		CORS:                  toCORS(b.Cors),
-		Encryption:            toBucketEncryption(b.Encryption),
-		Logging:               toBucketLogging(b.Logging),
-		Website:               toBucketWebsite(b.Website),
-		BucketPolicyOnly:      toBucketPolicyOnly(b.IamConfiguration),
-		Etag:                  b.Etag,
-		LocationType:          b.LocationType,
+		Name:                     b.Name,
+		Location:                 b.Location,
+		MetaGeneration:           b.Metageneration,
+		DefaultEventBasedHold:    b.DefaultEventBasedHold,
+		StorageClass:             b.StorageClass,
+		Created:                  convertTime(b.TimeCreated),
+		VersioningEnabled:        b.Versioning != nil && b.Versioning.Enabled,
+		ACL:                      toBucketACLRules(b.Acl),
+		DefaultObjectACL:         toObjectACLRules(b.DefaultObjectAcl),
+		Labels:                   b.Labels,
+		RequesterPays:            b.Billing != nil && b.Billing.RequesterPays,
+		Lifecycle:                toLifecycle(b.Lifecycle),
+		RetentionPolicy:          rp,
+		CORS:                     toCORS(b.Cors),
+		Encryption:               toBucketEncryption(b.Encryption),
+		Logging:                  toBucketLogging(b.Logging),
+		Website:                  toBucketWebsite(b.Website),
+		BucketPolicyOnly:         toBucketPolicyOnly(b.IamConfiguration),
+		UniformBucketLevelAccess: toUniformBucketLevelAccess(b.IamConfiguration),
+		Etag:                     b.Etag,
+		LocationType:             b.LocationType,
 	}, nil
 }
 
@@ -533,9 +553,9 @@
 		bb = &raw.BucketBilling{RequesterPays: true}
 	}
 	var bktIAM *raw.BucketIamConfiguration
-	if b.BucketPolicyOnly.Enabled {
+	if b.UniformBucketLevelAccess.Enabled || b.BucketPolicyOnly.Enabled {
 		bktIAM = &raw.BucketIamConfiguration{
-			BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
 				Enabled: true,
 			},
 		}
@@ -602,10 +622,20 @@
 	// newly created objects in this bucket.
 	DefaultEventBasedHold optional.Bool
 
-	// BucketPolicyOnly configures access checks to use only bucket-level IAM
-	// policies.
+	// BucketPolicyOnly is an alias for UniformBucketLevelAccess. Use of
+	// UniformBucketLevelAccess is recommended above the use of this field.
+	// Setting BucketPolicyOnly.Enabled OR UniformBucketLevelAccess.Enabled to
+	// true, will enable UniformBucketLevelAccess. If both BucketPolicyOnly and
+	// UniformBucketLevelAccess are set, the value of UniformBucketLevelAccess
+	// will take precedence.
 	BucketPolicyOnly *BucketPolicyOnly
 
+	// UniformBucketLevelAccess configures access checks to use only bucket-level IAM
+	// policies and ignore any ACL rules for the bucket.
+	// See https://cloud.google.com/storage/docs/uniform-bucket-level-access
+	// for more information.
+	UniformBucketLevelAccess *UniformBucketLevelAccess
+
 	// If set, updates the retention policy of the bucket. Using
 	// RetentionPolicy.RetentionPeriod = 0 will delete the existing policy.
 	//
@@ -694,9 +724,15 @@
 	}
 	if ua.BucketPolicyOnly != nil {
 		rb.IamConfiguration = &raw.BucketIamConfiguration{
-			BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
-				Enabled:         ua.BucketPolicyOnly.Enabled,
-				ForceSendFields: []string{"Enabled"},
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled: ua.BucketPolicyOnly.Enabled,
+			},
+		}
+	}
+	if ua.UniformBucketLevelAccess != nil {
+		rb.IamConfiguration = &raw.BucketIamConfiguration{
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled: ua.UniformBucketLevelAccess.Enabled,
 			},
 		}
 	}
@@ -1035,6 +1071,22 @@
 	}
 }
 
+func toUniformBucketLevelAccess(b *raw.BucketIamConfiguration) UniformBucketLevelAccess {
+	if b == nil || b.UniformBucketLevelAccess == nil || !b.UniformBucketLevelAccess.Enabled {
+		return UniformBucketLevelAccess{}
+	}
+	lt, err := time.Parse(time.RFC3339, b.UniformBucketLevelAccess.LockedTime)
+	if err != nil {
+		return UniformBucketLevelAccess{
+			Enabled: true,
+		}
+	}
+	return UniformBucketLevelAccess{
+		Enabled:    true,
+		LockedTime: lt,
+	}
+}
+
 // Objects returns an iterator over the objects in the bucket that match the Query q.
 // If q is nil, no filtering is done.
 //
diff --git a/storage/bucket_test.go b/storage/bucket_test.go
index 60b1501..ecf4341 100644
--- a/storage/bucket_test.go
+++ b/storage/bucket_test.go
@@ -39,8 +39,9 @@
 		RetentionPolicy: &RetentionPolicy{
 			RetentionPeriod: 3 * time.Second,
 		},
-		BucketPolicyOnly:  BucketPolicyOnly{Enabled: true},
-		VersioningEnabled: false,
+		BucketPolicyOnly:         BucketPolicyOnly{Enabled: true},
+		UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true},
+		VersioningEnabled:        false,
 		// should be ignored:
 		MetaGeneration: 39,
 		Created:        time.Now(),
@@ -105,7 +106,7 @@
 			RetentionPeriod: 3,
 		},
 		IamConfiguration: &raw.BucketIamConfiguration{
-			BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
 				Enabled: true,
 			},
 		},
@@ -168,17 +169,72 @@
 	if msg := testutil.Diff(got, want); msg != "" {
 		t.Error(msg)
 	}
+
+	// Test that setting either of BucketPolicyOnly or UniformBucketLevelAccess
+	// will enable UniformBucketLevelAccess.
+	// Set UBLA.Enabled = true --> UBLA should be set to enabled in the proto.
+	attrs.BucketPolicyOnly = BucketPolicyOnly{}
+	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true}
+	got = attrs.toRawBucket()
+	want.IamConfiguration = &raw.BucketIamConfiguration{
+		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+			Enabled: true,
+		},
+	}
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
+
+	// Set BucketPolicyOnly.Enabled = true --> UBLA should be set to enabled in
+	// the proto.
+	attrs.BucketPolicyOnly = BucketPolicyOnly{Enabled: true}
+	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{}
+	got = attrs.toRawBucket()
+	want.IamConfiguration = &raw.BucketIamConfiguration{
+		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+			Enabled: true,
+		},
+	}
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
+
+	// Set both BucketPolicyOnly.Enabled = true and
+	// UniformBucketLevelAccess.Enabled=true --> UBLA should be set to enabled
+	// in the proto.
+	attrs.BucketPolicyOnly = BucketPolicyOnly{Enabled: true}
+	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true}
+	got = attrs.toRawBucket()
+	want.IamConfiguration = &raw.BucketIamConfiguration{
+		UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+			Enabled: true,
+		},
+	}
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
+
+	// Set UBLA.Enabled=false and BucketPolicyOnly.Enabled=false --> UBLA
+	// should be disabled in the proto.
+	attrs.BucketPolicyOnly = BucketPolicyOnly{}
+	attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{}
+	got = attrs.toRawBucket()
+	want.IamConfiguration = nil
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
 }
 
 func TestBucketAttrsToUpdateToRawBucket(t *testing.T) {
 	t.Parallel()
 	au := &BucketAttrsToUpdate{
-		VersioningEnabled:     false,
-		RequesterPays:         false,
-		BucketPolicyOnly:      &BucketPolicyOnly{Enabled: false},
-		DefaultEventBasedHold: false,
-		RetentionPolicy:       &RetentionPolicy{RetentionPeriod: time.Hour},
-		Encryption:            &BucketEncryption{DefaultKMSKeyName: "key2"},
+		VersioningEnabled:        false,
+		RequesterPays:            false,
+		BucketPolicyOnly:         &BucketPolicyOnly{Enabled: false},
+		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: false},
+		DefaultEventBasedHold:    false,
+		RetentionPolicy:          &RetentionPolicy{RetentionPeriod: time.Hour},
+		Encryption:               &BucketEncryption{DefaultKMSKeyName: "key2"},
 		Lifecycle: &Lifecycle{
 			Rules: []LifecycleRule{
 				{
@@ -210,9 +266,8 @@
 		DefaultEventBasedHold: false,
 		RetentionPolicy:       &raw.BucketRetentionPolicy{RetentionPeriod: 3600},
 		IamConfiguration: &raw.BucketIamConfiguration{
-			BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
-				Enabled:         false,
-				ForceSendFields: []string{"Enabled"},
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled: false,
 			},
 		},
 		Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key2"},
@@ -259,6 +314,96 @@
 	if msg := testutil.Diff(got, want); msg != "" {
 		t.Error(msg)
 	}
+
+	// Test that setting either of BucketPolicyOnly or UniformBucketLevelAccess
+	// will enable UniformBucketLevelAccess.
+	// Set UBLA.Enabled = true --> UBLA should be set to enabled in the proto.
+	au4 := &BucketAttrsToUpdate{
+		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: true},
+	}
+	got = au4.toRawBucket()
+	want = &raw.Bucket{
+		IamConfiguration: &raw.BucketIamConfiguration{
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled: true,
+			},
+		},
+	}
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
+
+	// Set BucketPolicyOnly.Enabled = true --> UBLA should be set to enabled in
+	// the proto.
+	au5 := &BucketAttrsToUpdate{
+		BucketPolicyOnly: &BucketPolicyOnly{Enabled: true},
+	}
+	got = au5.toRawBucket()
+	want = &raw.Bucket{
+		IamConfiguration: &raw.BucketIamConfiguration{
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled: true,
+			},
+		},
+	}
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
+
+	// Set both BucketPolicyOnly.Enabled = true and
+	// UniformBucketLevelAccess.Enabled=true --> UBLA should be set to enabled
+	// in the proto.
+	au6 := &BucketAttrsToUpdate{
+		BucketPolicyOnly:         &BucketPolicyOnly{Enabled: true},
+		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: true},
+	}
+	got = au6.toRawBucket()
+	want = &raw.Bucket{
+		IamConfiguration: &raw.BucketIamConfiguration{
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled: true,
+			},
+		},
+	}
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
+
+	// Set UBLA.Enabled=false and BucketPolicyOnly.Enabled=false --> UBLA
+	// should be disabled in the proto.
+	au7 := &BucketAttrsToUpdate{
+		BucketPolicyOnly:         &BucketPolicyOnly{Enabled: false},
+		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: false},
+	}
+	got = au7.toRawBucket()
+	want = &raw.Bucket{
+		IamConfiguration: &raw.BucketIamConfiguration{
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled: false,
+			},
+		},
+	}
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
+
+	// UBLA.Enabled will have precedence above BucketPolicyOnly.Enabled if both
+	// are set with different values.
+	au8 := &BucketAttrsToUpdate{
+		BucketPolicyOnly:         &BucketPolicyOnly{Enabled: true},
+		UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: false},
+	}
+	got = au8.toRawBucket()
+	want = &raw.Bucket{
+		IamConfiguration: &raw.BucketIamConfiguration{
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled: false,
+			},
+		},
+	}
+	if msg := testutil.Diff(got, want); msg != "" {
+		t.Errorf(msg)
+	}
 }
 
 func TestCallBuilders(t *testing.T) {
@@ -392,6 +537,10 @@
 				Enabled:    true,
 				LockedTime: aTime.Format(time.RFC3339),
 			},
+			UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
+				Enabled:    true,
+				LockedTime: aTime.Format(time.RFC3339),
+			},
 		},
 		Cors: []*raw.BucketCors{
 			{
@@ -441,7 +590,8 @@
 			EffectiveTime:   aTime,
 			RetentionPeriod: 3 * time.Second,
 		},
-		BucketPolicyOnly: BucketPolicyOnly{Enabled: true, LockedTime: aTime},
+		BucketPolicyOnly:         BucketPolicyOnly{Enabled: true, LockedTime: aTime},
+		UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true, LockedTime: aTime},
 		CORS: []CORS{
 			{
 				MaxAge:          time.Hour,
diff --git a/storage/integration_test.go b/storage/integration_test.go
index 665b851..9b6e63a 100644
--- a/storage/integration_test.go
+++ b/storage/integration_test.go
@@ -434,6 +434,71 @@
 	}
 }
 
+func TestIntegration_UniformBucketLevelAccess(t *testing.T) {
+	ctx := context.Background()
+	client := testConfig(ctx, t)
+	defer client.Close()
+	h := testHelper{t}
+	bkt := client.Bucket(bucketName)
+
+	// Insert an object with custom ACL.
+	o := bkt.Object("uniformBucketLevelAccess")
+	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()
+	aclEntity := ACLEntity("user-test@example.com")
+	err := a.Set(ctx, aclEntity, RoleReader)
+	if err != nil {
+		t.Fatalf("set ACL failed: %v", err)
+	}
+
+	// Enable UniformBucketLevelAccess.
+	ua := BucketAttrsToUpdate{UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: true}}
+	attrs := h.mustUpdateBucket(bkt, ua)
+	if got, want := attrs.UniformBucketLevelAccess.Enabled, true; got != want {
+		t.Fatalf("got %v, want %v", got, want)
+	}
+	if got := attrs.UniformBucketLevelAccess.LockedTime; got.IsZero() {
+		t.Fatal("got a zero time value, want a populated value")
+	}
+
+	// Confirm BucketAccessControl returns error.
+	_, err = bkt.ACL().List(ctx)
+	if err == nil {
+		t.Fatal("expected Bucket ACL list to fail")
+	}
+
+	// Confirm ObjectAccessControl returns error.
+	_, err = o.ACL().List(ctx)
+	if err == nil {
+		t.Fatal("expected Object ACL list to fail")
+	}
+
+	// Disable UniformBucketLevelAccess.
+	ua = BucketAttrsToUpdate{UniformBucketLevelAccess: &UniformBucketLevelAccess{Enabled: false}}
+	attrs = h.mustUpdateBucket(bkt, ua)
+	if got, want := attrs.UniformBucketLevelAccess.Enabled, false; got != want {
+		t.Fatalf("got %v, want %v", got, want)
+	}
+
+	// Check that the object ACLs are the same.
+	acls, err := o.ACL().List(ctx)
+	if err != nil {
+		t.Fatalf("object ACL list failed: %v", err)
+	}
+
+	// Check that ACL rules contain custom ACL from above.
+	if !containsACL(acls, aclEntity, RoleReader) {
+		t.Fatalf("expected ACLs %v to include custom ACL entity %v", acls, aclEntity)
+	}
+}
+
 func containsACL(acls []ACLRule, e ACLEntity, r ACLRole) bool {
 	for _, a := range acls {
 		if a.Entity == e && a.Role == r {