blob: de9d2f91426a5e68a5eed3e5738c2496868c17ef [file] [log] [blame]
// Copyright 2017 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package storage
import (
"context"
"fmt"
"testing"
"time"
"cloud.google.com/go/internal/testutil"
"cloud.google.com/go/storage/internal/apiv2/storagepb"
"github.com/google/go-cmp/cmp"
gax "github.com/googleapis/gax-go/v2"
"golang.org/x/oauth2/google"
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
raw "google.golang.org/api/storage/v1"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
)
func TestBucketAttrsToRawBucket(t *testing.T) {
t.Parallel()
attrs := &BucketAttrs{
Name: "name",
ACL: []ACLRule{{Entity: "bob@example.com", Role: RoleOwner, Domain: "d", Email: "e"}},
DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader, EntityID: "eid",
ProjectTeam: &ProjectTeam{ProjectNumber: "17", Team: "t"}}},
Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
Location: "loc",
StorageClass: "class",
RetentionPolicy: &RetentionPolicy{
RetentionPeriod: 3 * time.Second,
},
BucketPolicyOnly: BucketPolicyOnly{Enabled: true},
UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true},
PublicAccessPrevention: PublicAccessPreventionEnforced,
VersioningEnabled: false,
RPO: RPOAsyncTurbo,
// should be ignored:
MetaGeneration: 39,
Created: time.Now(),
Labels: map[string]string{"label": "value"},
CORS: []CORS{
{
MaxAge: time.Hour,
Methods: []string{"GET", "POST"},
Origins: []string{"*"},
ResponseHeaders: []string{"FOO"},
},
},
Encryption: &BucketEncryption{DefaultKMSKeyName: "key"},
Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
Autoclass: &Autoclass{Enabled: true, TerminalStorageClass: "NEARLINE"},
SoftDeletePolicy: &SoftDeletePolicy{RetentionDuration: time.Hour},
HierarchicalNamespace: &HierarchicalNamespace{Enabled: true},
Lifecycle: Lifecycle{
Rules: []LifecycleRule{{
Action: LifecycleAction{
Type: SetStorageClassAction,
StorageClass: "NEARLINE",
},
Condition: LifecycleCondition{
AgeInDays: 10,
Liveness: Live,
CreatedBefore: time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
MatchesStorageClasses: []string{"STANDARD"},
NumNewerVersions: 3,
},
}, {
Action: LifecycleAction{
Type: SetStorageClassAction,
StorageClass: "ARCHIVE",
},
Condition: LifecycleCondition{
CustomTimeBefore: time.Date(2020, 1, 2, 3, 0, 0, 0, time.UTC),
DaysSinceCustomTime: 100,
Liveness: Live,
MatchesStorageClasses: []string{"STANDARD"},
},
}, {
Action: LifecycleAction{
Type: DeleteAction,
},
Condition: LifecycleCondition{
DaysSinceNoncurrentTime: 30,
Liveness: Live,
NoncurrentTimeBefore: time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
MatchesStorageClasses: []string{"NEARLINE"},
NumNewerVersions: 10,
},
}, {
Action: LifecycleAction{
Type: DeleteAction,
},
Condition: LifecycleCondition{
AgeInDays: 10,
MatchesPrefix: []string{"testPrefix"},
MatchesSuffix: []string{"testSuffix"},
NumNewerVersions: 3,
},
}, {
Action: LifecycleAction{
Type: DeleteAction,
},
Condition: LifecycleCondition{
Liveness: Archived,
},
}, {
Action: LifecycleAction{
Type: AbortIncompleteMPUAction,
},
Condition: LifecycleCondition{
AgeInDays: 20,
},
}, {
Action: LifecycleAction{
Type: DeleteAction,
},
Condition: LifecycleCondition{
AllObjects: true,
},
}},
},
}
got := attrs.toRawBucket()
want := &raw.Bucket{
Name: "name",
Acl: []*raw.BucketAccessControl{
{Entity: "bob@example.com", Role: "OWNER"}, // other fields ignored on create/update
},
DefaultObjectAcl: []*raw.ObjectAccessControl{
{Entity: "allUsers", Role: "READER"}, // other fields ignored on create/update
},
Location: "loc",
StorageClass: "class",
RetentionPolicy: &raw.BucketRetentionPolicy{
RetentionPeriod: 3,
},
IamConfiguration: &raw.BucketIamConfiguration{
UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
Enabled: true,
},
PublicAccessPrevention: "enforced",
},
Versioning: nil, // ignore VersioningEnabled if false
Rpo: rpoAsyncTurbo,
Labels: map[string]string{"label": "value"},
Cors: []*raw.BucketCors{
{
MaxAgeSeconds: 3600,
Method: []string{"GET", "POST"},
Origin: []string{"*"},
ResponseHeader: []string{"FOO"},
},
},
Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key"},
Logging: &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
Website: &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
Autoclass: &raw.BucketAutoclass{Enabled: true, TerminalStorageClass: "NEARLINE"},
SoftDeletePolicy: &raw.BucketSoftDeletePolicy{RetentionDurationSeconds: 60 * 60},
HierarchicalNamespace: &raw.BucketHierarchicalNamespace{Enabled: true},
Lifecycle: &raw.BucketLifecycle{
Rule: []*raw.BucketLifecycleRule{{
Action: &raw.BucketLifecycleRuleAction{
Type: SetStorageClassAction,
StorageClass: "NEARLINE",
},
Condition: &raw.BucketLifecycleRuleCondition{
Age: googleapi.Int64(10),
IsLive: googleapi.Bool(true),
CreatedBefore: "2017-01-02",
MatchesStorageClass: []string{"STANDARD"},
NumNewerVersions: 3,
},
},
{
Action: &raw.BucketLifecycleRuleAction{
StorageClass: "ARCHIVE",
Type: SetStorageClassAction,
},
Condition: &raw.BucketLifecycleRuleCondition{
IsLive: googleapi.Bool(true),
CustomTimeBefore: "2020-01-02",
DaysSinceCustomTime: 100,
MatchesStorageClass: []string{"STANDARD"},
},
},
{
Action: &raw.BucketLifecycleRuleAction{
Type: DeleteAction,
},
Condition: &raw.BucketLifecycleRuleCondition{
DaysSinceNoncurrentTime: 30,
IsLive: googleapi.Bool(true),
NoncurrentTimeBefore: "2017-01-02",
MatchesStorageClass: []string{"NEARLINE"},
NumNewerVersions: 10,
},
},
{
Action: &raw.BucketLifecycleRuleAction{
Type: DeleteAction,
},
Condition: &raw.BucketLifecycleRuleCondition{
Age: googleapi.Int64(10),
MatchesPrefix: []string{"testPrefix"},
MatchesSuffix: []string{"testSuffix"},
NumNewerVersions: 3,
},
},
{
Action: &raw.BucketLifecycleRuleAction{
Type: DeleteAction,
},
Condition: &raw.BucketLifecycleRuleCondition{
IsLive: googleapi.Bool(false),
},
},
{
Action: &raw.BucketLifecycleRuleAction{
Type: AbortIncompleteMPUAction,
},
Condition: &raw.BucketLifecycleRuleCondition{
Age: googleapi.Int64(20),
},
},
{
Action: &raw.BucketLifecycleRuleAction{
Type: DeleteAction,
},
Condition: &raw.BucketLifecycleRuleCondition{
Age: googleapi.Int64(0),
ForceSendFields: []string{"Age"},
},
},
},
},
}
if msg := testutil.Diff(got, want); msg != "" {
t.Error(msg)
}
attrs.VersioningEnabled = true
attrs.RequesterPays = true
got = attrs.toRawBucket()
want.Versioning = &raw.BucketVersioning{Enabled: true}
want.Billing = &raw.BucketBilling{RequesterPays: true}
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,
},
PublicAccessPrevention: "enforced",
}
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,
},
PublicAccessPrevention: "enforced",
}
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,
},
PublicAccessPrevention: "enforced",
}
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 = &raw.BucketIamConfiguration{
PublicAccessPrevention: "enforced",
}
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
// Test that setting PublicAccessPrevention to "unspecified" leads to the
// inherited setting being propagated in the proto.
attrs.PublicAccessPrevention = PublicAccessPreventionUnspecified
got = attrs.toRawBucket()
want.IamConfiguration = &raw.BucketIamConfiguration{
PublicAccessPrevention: "inherited",
}
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
// Test that setting PublicAccessPrevention to "inherited" leads to the
// setting being propagated in the proto.
attrs.PublicAccessPrevention = PublicAccessPreventionInherited
got = attrs.toRawBucket()
want.IamConfiguration = &raw.BucketIamConfiguration{
PublicAccessPrevention: "inherited",
}
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
// Test that setting RPO to default is propagated in the proto.
attrs.RPO = RPODefault
got = attrs.toRawBucket()
want.Rpo = rpoDefault
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: "inherited",
}
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)
}
}
func TestBucketAttrsToUpdateToRawBucket(t *testing.T) {
t.Parallel()
au := &BucketAttrsToUpdate{
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{
{
Action: LifecycleAction{Type: "Delete"},
Condition: LifecycleCondition{AgeInDays: 30},
},
{
Action: LifecycleAction{Type: AbortIncompleteMPUAction},
Condition: LifecycleCondition{AgeInDays: 13},
},
},
},
Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
StorageClass: "NEARLINE",
Autoclass: &Autoclass{Enabled: true, TerminalStorageClass: "ARCHIVE"},
SoftDeletePolicy: &SoftDeletePolicy{RetentionDuration: time.Hour},
}
au.SetLabel("a", "foo")
au.DeleteLabel("b")
au.SetLabel("c", "")
got := au.toRawBucket()
want := &raw.Bucket{
Versioning: &raw.BucketVersioning{
Enabled: false,
ForceSendFields: []string{"Enabled"},
},
Labels: map[string]string{
"a": "foo",
"c": "",
},
Billing: &raw.BucketBilling{
RequesterPays: false,
ForceSendFields: []string{"RequesterPays"},
},
DefaultEventBasedHold: false,
RetentionPolicy: &raw.BucketRetentionPolicy{RetentionPeriod: 3600},
IamConfiguration: &raw.BucketIamConfiguration{
UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
Enabled: false,
ForceSendFields: []string{"Enabled"},
},
},
Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key2"},
NullFields: []string{"Labels.b"},
Lifecycle: &raw.BucketLifecycle{
Rule: []*raw.BucketLifecycleRule{
{
Action: &raw.BucketLifecycleRuleAction{Type: "Delete"},
Condition: &raw.BucketLifecycleRuleCondition{Age: googleapi.Int64(30)},
},
{
Action: &raw.BucketLifecycleRuleAction{Type: AbortIncompleteMPUAction},
Condition: &raw.BucketLifecycleRuleCondition{Age: googleapi.Int64(13)},
},
},
},
Logging: &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
Website: &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
StorageClass: "NEARLINE",
Autoclass: &raw.BucketAutoclass{Enabled: true, TerminalStorageClass: "ARCHIVE", ForceSendFields: []string{"Enabled"}},
SoftDeletePolicy: &raw.BucketSoftDeletePolicy{RetentionDurationSeconds: 3600},
ForceSendFields: []string{"DefaultEventBasedHold", "Lifecycle", "Autoclass"},
}
if msg := testutil.Diff(got, want); msg != "" {
t.Error(msg)
}
var au2 BucketAttrsToUpdate
au2.DeleteLabel("b")
got = au2.toRawBucket()
want = &raw.Bucket{
Labels: map[string]string{},
ForceSendFields: []string{"Labels"},
NullFields: []string{"Labels.b"},
}
if msg := testutil.Diff(got, want); msg != "" {
t.Error(msg)
}
// Test nulls.
au3 := &BucketAttrsToUpdate{
RetentionPolicy: &RetentionPolicy{},
Encryption: &BucketEncryption{},
Logging: &BucketLogging{},
Website: &BucketWebsite{},
SoftDeletePolicy: &SoftDeletePolicy{},
}
got = au3.toRawBucket()
want = &raw.Bucket{
NullFields: []string{"RetentionPolicy", "Encryption", "Logging", "Website", "SoftDeletePolicy"},
}
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,
ForceSendFields: []string{"Enabled"},
},
},
}
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,
ForceSendFields: []string{"Enabled"},
},
},
}
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,
ForceSendFields: []string{"Enabled"},
},
},
}
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,
ForceSendFields: []string{"Enabled"},
},
},
}
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,
ForceSendFields: []string{"Enabled"},
},
},
}
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
// Set an empty Lifecycle and verify that it will be sent.
au9 := &BucketAttrsToUpdate{
Lifecycle: &Lifecycle{},
}
got = au9.toRawBucket()
want = &raw.Bucket{
Lifecycle: &raw.BucketLifecycle{
ForceSendFields: []string{"Rule"},
},
ForceSendFields: []string{"Lifecycle"},
}
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
}
func TestNewBucket(t *testing.T) {
labels := map[string]string{"a": "b"}
matchClasses := []string{"STANDARD"}
aTime := time.Date(2017, 1, 2, 0, 0, 0, 0, time.UTC)
rb := &raw.Bucket{
Name: "name",
Location: "loc",
DefaultEventBasedHold: true,
Metageneration: 3,
StorageClass: "sc",
TimeCreated: "2017-10-23T04:05:06Z",
Versioning: &raw.BucketVersioning{Enabled: true},
Labels: labels,
Billing: &raw.BucketBilling{RequesterPays: true},
Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
Lifecycle: &raw.BucketLifecycle{
Rule: []*raw.BucketLifecycleRule{{
Action: &raw.BucketLifecycleRuleAction{
Type: "SetStorageClass",
StorageClass: "NEARLINE",
},
Condition: &raw.BucketLifecycleRuleCondition{
Age: googleapi.Int64(10),
IsLive: googleapi.Bool(true),
CreatedBefore: "2017-01-02",
MatchesStorageClass: matchClasses,
NumNewerVersions: 3,
},
}},
},
RetentionPolicy: &raw.BucketRetentionPolicy{
RetentionPeriod: 3,
EffectiveTime: aTime.Format(time.RFC3339),
},
ObjectRetention: &raw.BucketObjectRetention{
Mode: "Enabled",
},
IamConfiguration: &raw.BucketIamConfiguration{
BucketPolicyOnly: &raw.BucketIamConfigurationBucketPolicyOnly{
Enabled: true,
LockedTime: aTime.Format(time.RFC3339),
},
UniformBucketLevelAccess: &raw.BucketIamConfigurationUniformBucketLevelAccess{
Enabled: true,
LockedTime: aTime.Format(time.RFC3339),
},
},
Cors: []*raw.BucketCors{
{
MaxAgeSeconds: 3600,
Method: []string{"GET", "POST"},
Origin: []string{"*"},
ResponseHeader: []string{"FOO"},
},
},
Acl: []*raw.BucketAccessControl{
{Bucket: "name", Role: "READER", Email: "joe@example.com", Entity: "allUsers"},
},
LocationType: "dual-region",
Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key"},
Logging: &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
Website: &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
ProjectNumber: 123231313,
Autoclass: &raw.BucketAutoclass{
Enabled: true,
ToggleTime: "2017-10-23T04:05:06Z",
TerminalStorageClass: "NEARLINE",
TerminalStorageClassUpdateTime: "2017-10-23T04:05:06Z",
},
SoftDeletePolicy: &raw.BucketSoftDeletePolicy{
EffectiveTime: "2017-10-23T04:05:06Z",
RetentionDurationSeconds: 3600,
},
HierarchicalNamespace: &raw.BucketHierarchicalNamespace{Enabled: true},
}
want := &BucketAttrs{
Name: "name",
Location: "loc",
DefaultEventBasedHold: true,
MetaGeneration: 3,
StorageClass: "sc",
Created: time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
VersioningEnabled: true,
Labels: labels,
Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
RequesterPays: true,
Lifecycle: Lifecycle{
Rules: []LifecycleRule{
{
Action: LifecycleAction{
Type: SetStorageClassAction,
StorageClass: "NEARLINE",
},
Condition: LifecycleCondition{
AgeInDays: 10,
Liveness: Live,
CreatedBefore: time.Date(2017, 1, 2, 0, 0, 0, 0, time.UTC),
MatchesStorageClasses: matchClasses,
NumNewerVersions: 3,
},
},
},
},
RetentionPolicy: &RetentionPolicy{
EffectiveTime: aTime,
RetentionPeriod: 3 * time.Second,
},
ObjectRetentionMode: "Enabled",
BucketPolicyOnly: BucketPolicyOnly{Enabled: true, LockedTime: aTime},
UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true, LockedTime: aTime},
CORS: []CORS{
{
MaxAge: time.Hour,
Methods: []string{"GET", "POST"},
Origins: []string{"*"},
ResponseHeaders: []string{"FOO"},
},
},
Encryption: &BucketEncryption{DefaultKMSKeyName: "key"},
Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
ACL: []ACLRule{{Entity: "allUsers", Role: RoleReader, Email: "joe@example.com"}},
DefaultObjectACL: nil,
LocationType: "dual-region",
ProjectNumber: 123231313,
Autoclass: &Autoclass{
Enabled: true,
ToggleTime: time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
TerminalStorageClass: "NEARLINE",
TerminalStorageClassUpdateTime: time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
},
SoftDeletePolicy: &SoftDeletePolicy{
EffectiveTime: time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC),
RetentionDuration: time.Hour,
},
HierarchicalNamespace: &HierarchicalNamespace{Enabled: true},
}
got, err := newBucket(rb)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got=-, want=+:\n%s", diff)
}
}
func TestNewBucketFromProto(t *testing.T) {
autoclassTSC := "NEARLINE"
pb := &storagepb.Bucket{
Name: "name",
Acl: []*storagepb.BucketAccessControl{
{Entity: "bob@example.com", Role: "OWNER"},
},
DefaultObjectAcl: []*storagepb.ObjectAccessControl{
{Entity: "allUsers", Role: "READER"},
},
Location: "loc",
LocationType: "region",
StorageClass: "class",
RetentionPolicy: &storagepb.Bucket_RetentionPolicy{
RetentionDuration: durationpb.New(3 * time.Second),
EffectiveTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
IamConfig: &storagepb.Bucket_IamConfig{
UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
Enabled: true,
LockTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
PublicAccessPrevention: "enforced",
},
Rpo: rpoAsyncTurbo,
Metageneration: int64(39),
CreateTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
Labels: map[string]string{"label": "value"},
Cors: []*storagepb.Bucket_Cors{
{
MaxAgeSeconds: 3600,
Method: []string{"GET", "POST"},
Origin: []string{"*"},
ResponseHeader: []string{"FOO"},
},
},
Encryption: &storagepb.Bucket_Encryption{DefaultKmsKey: "key"},
Logging: &storagepb.Bucket_Logging{LogBucket: "projects/_/buckets/lb", LogObjectPrefix: "p"},
Website: &storagepb.Bucket_Website{MainPageSuffix: "mps", NotFoundPage: "404"},
Autoclass: &storagepb.Bucket_Autoclass{
Enabled: true,
ToggleTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
TerminalStorageClass: &autoclassTSC,
TerminalStorageClassUpdateTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
SoftDeletePolicy: &storagepb.Bucket_SoftDeletePolicy{
RetentionDuration: durationpb.New(3 * time.Hour),
EffectiveTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
HierarchicalNamespace: &storagepb.Bucket_HierarchicalNamespace{
Enabled: true,
},
Lifecycle: &storagepb.Bucket_Lifecycle{
Rule: []*storagepb.Bucket_Lifecycle_Rule{
{
Action: &storagepb.Bucket_Lifecycle_Rule_Action{Type: "Delete"},
Condition: &storagepb.Bucket_Lifecycle_Rule_Condition{
AgeDays: proto.Int32(int32(10)),
},
},
},
},
}
want := &BucketAttrs{
Name: "name",
ACL: []ACLRule{{Entity: "bob@example.com", Role: RoleOwner}},
DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader}},
Location: "loc",
LocationType: "region",
StorageClass: "class",
RetentionPolicy: &RetentionPolicy{
RetentionPeriod: 3 * time.Second,
EffectiveTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
},
BucketPolicyOnly: BucketPolicyOnly{Enabled: true, LockedTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},
UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true, LockedTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},
PublicAccessPrevention: PublicAccessPreventionEnforced,
RPO: RPOAsyncTurbo,
MetaGeneration: 39,
Created: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
Labels: map[string]string{"label": "value"},
CORS: []CORS{
{
MaxAge: time.Hour,
Methods: []string{"GET", "POST"},
Origins: []string{"*"},
ResponseHeaders: []string{"FOO"},
},
},
Encryption: &BucketEncryption{DefaultKMSKeyName: "key"},
Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
Autoclass: &Autoclass{Enabled: true, ToggleTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), TerminalStorageClass: "NEARLINE", TerminalStorageClassUpdateTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},
SoftDeletePolicy: &SoftDeletePolicy{
EffectiveTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
RetentionDuration: time.Hour * 3,
},
HierarchicalNamespace: &HierarchicalNamespace{
Enabled: true,
},
Lifecycle: Lifecycle{
Rules: []LifecycleRule{{
Action: LifecycleAction{
Type: DeleteAction,
},
Condition: LifecycleCondition{
AgeInDays: 10,
},
}},
},
}
got := newBucketFromProto(pb)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got=-, want=+:\n%s", diff)
}
}
func TestBucketAttrsToProtoBucket(t *testing.T) {
t.Parallel()
attrs := &BucketAttrs{
Name: "name",
ACL: []ACLRule{{Entity: "bob@example.com", Role: RoleOwner, Domain: "d", Email: "e"}},
DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader, EntityID: "eid",
ProjectTeam: &ProjectTeam{ProjectNumber: "17", Team: "t"}}},
Location: "loc",
StorageClass: "class",
RetentionPolicy: &RetentionPolicy{
RetentionPeriod: 3 * time.Second,
},
BucketPolicyOnly: BucketPolicyOnly{Enabled: true},
UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true},
PublicAccessPrevention: PublicAccessPreventionEnforced,
VersioningEnabled: false,
RPO: RPOAsyncTurbo,
Created: time.Now(),
Labels: map[string]string{"label": "value"},
CORS: []CORS{
{
MaxAge: time.Hour,
Methods: []string{"GET", "POST"},
Origins: []string{"*"},
ResponseHeaders: []string{"FOO"},
},
},
Encryption: &BucketEncryption{DefaultKMSKeyName: "key"},
Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"},
Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"},
Autoclass: &Autoclass{Enabled: true, TerminalStorageClass: "ARCHIVE"},
SoftDeletePolicy: &SoftDeletePolicy{RetentionDuration: time.Hour * 2},
HierarchicalNamespace: &HierarchicalNamespace{Enabled: true},
Lifecycle: Lifecycle{
Rules: []LifecycleRule{{
Action: LifecycleAction{
Type: DeleteAction,
},
Condition: LifecycleCondition{
AgeInDays: 10,
},
}},
},
// Below fields should be ignored.
MetaGeneration: 39,
Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0",
}
got := attrs.toProtoBucket()
autoclassTSC := "ARCHIVE"
want := &storagepb.Bucket{
Name: "name",
Acl: []*storagepb.BucketAccessControl{
{Entity: "bob@example.com", Role: "OWNER"},
},
DefaultObjectAcl: []*storagepb.ObjectAccessControl{
{Entity: "allUsers", Role: "READER"},
},
Location: "loc",
StorageClass: "class",
RetentionPolicy: &storagepb.Bucket_RetentionPolicy{
RetentionDuration: durationpb.New(3 * time.Second),
},
IamConfig: &storagepb.Bucket_IamConfig{
UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
Enabled: true,
},
PublicAccessPrevention: "enforced",
},
Versioning: nil, // ignore VersioningEnabled if false
Rpo: rpoAsyncTurbo,
Labels: map[string]string{"label": "value"},
Cors: []*storagepb.Bucket_Cors{
{
MaxAgeSeconds: 3600,
Method: []string{"GET", "POST"},
Origin: []string{"*"},
ResponseHeader: []string{"FOO"},
},
},
Encryption: &storagepb.Bucket_Encryption{DefaultKmsKey: "key"},
Logging: &storagepb.Bucket_Logging{LogBucket: "projects/_/buckets/lb", LogObjectPrefix: "p"},
Website: &storagepb.Bucket_Website{MainPageSuffix: "mps", NotFoundPage: "404"},
Autoclass: &storagepb.Bucket_Autoclass{Enabled: true, TerminalStorageClass: &autoclassTSC},
SoftDeletePolicy: &storagepb.Bucket_SoftDeletePolicy{RetentionDuration: durationpb.New(2 * time.Hour)},
HierarchicalNamespace: &storagepb.Bucket_HierarchicalNamespace{Enabled: true},
Lifecycle: &storagepb.Bucket_Lifecycle{
Rule: []*storagepb.Bucket_Lifecycle_Rule{
{
Action: &storagepb.Bucket_Lifecycle_Rule_Action{Type: "Delete"},
Condition: &storagepb.Bucket_Lifecycle_Rule_Condition{
AgeDays: proto.Int32(int32(10)),
NumNewerVersions: proto.Int32(int32(0)),
DaysSinceCustomTime: proto.Int32(int32(0)),
DaysSinceNoncurrentTime: proto.Int32(int32(0)),
},
},
},
},
}
if msg := testutil.Diff(got, want); msg != "" {
t.Error(msg)
}
attrs.VersioningEnabled = true
attrs.RequesterPays = true
got = attrs.toProtoBucket()
want.Versioning = &storagepb.Bucket_Versioning{Enabled: true}
want.Billing = &storagepb.Bucket_Billing{RequesterPays: true}
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.toProtoBucket()
want.IamConfig = &storagepb.Bucket_IamConfig{
UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
Enabled: true,
},
PublicAccessPrevention: "enforced",
}
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.toProtoBucket()
want.IamConfig = &storagepb.Bucket_IamConfig{
UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
Enabled: true,
},
PublicAccessPrevention: "enforced",
}
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.toProtoBucket()
want.IamConfig = &storagepb.Bucket_IamConfig{
UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
Enabled: true,
},
PublicAccessPrevention: "enforced",
}
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.toProtoBucket()
want.IamConfig = &storagepb.Bucket_IamConfig{
PublicAccessPrevention: "enforced",
}
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
// Test that setting PublicAccessPrevention to "unspecified" leads to the
// inherited setting being propagated in the proto.
attrs.PublicAccessPrevention = PublicAccessPreventionUnspecified
got = attrs.toProtoBucket()
want.IamConfig = &storagepb.Bucket_IamConfig{
PublicAccessPrevention: "inherited",
}
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
// Test that setting PublicAccessPrevention to "inherited" leads to the
// setting being propagated in the proto.
attrs.PublicAccessPrevention = PublicAccessPreventionInherited
got = attrs.toProtoBucket()
want.IamConfig = &storagepb.Bucket_IamConfig{
PublicAccessPrevention: "inherited",
}
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
// Test that setting RPO to default is propagated in the proto.
attrs.RPO = RPODefault
got = attrs.toProtoBucket()
want.Rpo = rpoDefault
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.toProtoBucket()
want.IamConfig = &storagepb.Bucket_IamConfig{
UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{
Enabled: true,
},
PublicAccessPrevention: "inherited",
}
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.toProtoBucket()
want.IamConfig = nil
if msg := testutil.Diff(got, want); msg != "" {
t.Errorf(msg)
}
}
func TestBucketRetryer(t *testing.T) {
testCases := []struct {
name string
call func(b *BucketHandle) *BucketHandle
want *retryConfig
}{
{
name: "all defaults",
call: func(b *BucketHandle) *BucketHandle {
return b.Retryer()
},
want: &retryConfig{},
},
{
name: "set all options",
call: func(b *BucketHandle) *BucketHandle {
return b.Retryer(
WithBackoff(gax.Backoff{
Initial: 2 * time.Second,
Max: 30 * time.Second,
Multiplier: 3,
}),
WithPolicy(RetryAlways),
WithMaxAttempts(5),
WithErrorFunc(func(err error) bool { return false }))
},
want: &retryConfig{
backoff: &gax.Backoff{
Initial: 2 * time.Second,
Max: 30 * time.Second,
Multiplier: 3,
},
policy: RetryAlways,
maxAttempts: expectedAttempts(5),
shouldRetry: func(err error) bool { return false },
},
},
{
name: "set some backoff options",
call: func(b *BucketHandle) *BucketHandle {
return b.Retryer(
WithBackoff(gax.Backoff{
Multiplier: 3,
}))
},
want: &retryConfig{
backoff: &gax.Backoff{
Multiplier: 3,
}},
},
{
name: "set policy only",
call: func(b *BucketHandle) *BucketHandle {
return b.Retryer(WithPolicy(RetryNever))
},
want: &retryConfig{
policy: RetryNever,
},
},
{
name: "set max retry attempts only",
call: func(b *BucketHandle) *BucketHandle {
return b.Retryer(WithMaxAttempts(5))
},
want: &retryConfig{
maxAttempts: expectedAttempts(5),
},
},
{
name: "set ErrorFunc only",
call: func(b *BucketHandle) *BucketHandle {
return b.Retryer(
WithErrorFunc(func(err error) bool { return false }))
},
want: &retryConfig{
shouldRetry: func(err error) bool { return false },
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(s *testing.T) {
b := tc.call(&BucketHandle{})
if diff := cmp.Diff(
b.retry,
tc.want,
cmp.AllowUnexported(retryConfig{}, gax.Backoff{}),
// ErrorFunc cannot be compared directly, but we check if both are
// either nil or non-nil.
cmp.Comparer(func(a, b func(err error) bool) bool {
return (a == nil && b == nil) || (a != nil && b != nil)
}),
); diff != "" {
s.Fatalf("retry not configured correctly: %v", diff)
}
})
}
}
func TestDetectDefaultGoogleAccessID(t *testing.T) {
testCases := []struct {
name string
serviceAccount string
creds func(string) string
expectSuccess bool
}{
{
name: "impersonated creds",
serviceAccount: "default@my-project.iam.gserviceaccount.com",
creds: func(sa string) string {
return fmt.Sprintf(`{
"delegates": [],
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken",
"source_credentials": {
"client_id": "id",
"client_secret": "secret",
"refresh_token": "token",
"type": "authorized_user"
},
"type": "impersonated_service_account"
}`, sa)
},
expectSuccess: true,
},
{
name: "gcloud ADC creds",
serviceAccount: "default@my-project.iam.gserviceaccount.com",
creds: func(sa string) string {
return fmt.Sprint(`{
"client_id": "my-id.apps.googleusercontent.com",
"client_secret": "secret",
"quota_project_id": "",
"refresh_token": "token",
"type": "authorized_user"
}`)
},
expectSuccess: false,
},
{
name: "ADC private key",
serviceAccount: "default@my-project.iam.gserviceaccount.com",
creds: func(sa string) string {
return fmt.Sprintf(`{
"type": "service_account",
"project_id": "my-project",
"private_key_id": "my1",
"private_key": "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----\n",
"client_email": "%s",
"client_id": "01",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "cert"
}`, sa)
},
expectSuccess: true,
},
{
name: "no creds",
creds: func(_ string) string {
return ""
},
expectSuccess: false,
},
{
name: "malformed creds",
serviceAccount: "default@my-project.iam.gserviceaccount.com",
creds: func(sa string) string {
return fmt.Sprintf(`{
"type": "service_account"
"project_id": "my-project",
"private_key_id": "my1",
"private_key": "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----\n",
"client_email": "%s",
}`, sa)
},
expectSuccess: false,
},
{
name: "external creds",
serviceAccount: "default@my-project.iam.gserviceaccount.com",
creds: func(sa string) string {
return fmt.Sprintf(`{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "id",
"region_url": "region_url",
"url": "url",
"regional_cred_verification_url": "ver_url",
"imdsv2_session_token_url": "tok_url"
}
}`, sa)
},
expectSuccess: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
bucket := BucketHandle{
c: &Client{
creds: &google.Credentials{
JSON: []byte(tc.creds(tc.serviceAccount)),
},
},
name: "my-bucket",
}
id, err := bucket.detectDefaultGoogleAccessID()
if tc.expectSuccess {
if err != nil {
t.Fatal(err)
}
if id != tc.serviceAccount {
t.Errorf("service account not found correctly; got: %s, want: %s", id, tc.serviceAccount)
}
} else if err == nil {
t.Error("expected error but detectDefaultGoogleAccessID did not return one")
}
})
}
}
// TestBucketSignedURL_Endpoint_Emulator_Host tests that Bucket.SignedURl
// respects the host set in STORAGE_EMULATOR_HOST and/or in option.WithEndpoint
// TODO: move this testing to conformance tests.
func TestBucketSignedURL_Endpoint_Emulator_Host(t *testing.T) {
expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
bucketName := "bucket-name"
objectName := "obj-name"
localhost9000 := "localhost:9000"
localhost6000Https := "https://localhost:6000"
tests := []struct {
desc string
emulatorHost string
endpoint *string
now time.Time
opts *SignedURLOptions
// Note for future implementors: X-Goog-Signature generated by having
// the client run through its algorithm with pre-defined input and copy
// pasting the output. These tests are not great for testing whether
// the right signature is calculated - instead we rely on the backend
// and integration tests for that.
want string
}{
{
desc: "SignURLV4 creates link to resources in emulator",
emulatorHost: localhost9000,
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4,
Insecure: true,
},
want: "http://localhost:9000/" + bucketName + "/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "SignURLV4 - endpoint",
endpoint: &localhost9000,
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4,
Insecure: true,
},
want: "http://localhost:9000/" + bucketName + "/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "SignURLV4 - endpoint takes precedence over emulator",
endpoint: &localhost9000,
emulatorHost: "localhost:8000",
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4,
Insecure: true,
},
want: "http://localhost:9000/" + bucketName + "/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "SigningSchemeV2 - emulator",
emulatorHost: "localhost:8000",
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV2,
},
want: "https://localhost:8000/" + bucketName + "/" + objectName +
"?Expires=1033570800" +
"&GoogleAccessId=xxx%40clientid" +
"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
},
{
desc: "SigningSchemeV2 - endpoint",
emulatorHost: "localhost:8000",
endpoint: &localhost9000,
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV2,
},
want: "https://localhost:9000/" + bucketName + "/" + objectName +
"?Expires=1033570800" +
"&GoogleAccessId=xxx%40clientid" +
"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
},
{
desc: "VirtualHostedStyle - emulator",
emulatorHost: "localhost:8000",
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4,
Style: VirtualHostedStyle(),
Insecure: true,
},
want: "http://" + bucketName + ".localhost:8000/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=35e0b9d33901a2518956821175f88c2c4eb3f4461b725af74b37c36d23f8bbe927558ac57b0be40d345f20bca55ba0652d38b7a620f8da68d4f733706ad104da468c3a039459acf35f3022e388760cd49893c998c33fe3ccc8c022d7034ab98bdbdcac4b680bb24ae5ed586a42ee9495a873ffc484e297853a8a3892d0d6385c980cb7e3c5c8bdd4939b4c17105f10fe8b5b9744017bf59431ff176c1550ae1c64ddd6628096eb6895c97c5da4d850aca72c14b7f5018c15b34d4b00ec63ff2ccb688ddbef2d32648e247ffd0137498080f320f293eb811a94fb526227324bbbd01335446388797803e67d802f97b52565deba3d2387ecabf4f3094662236017" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "VirtualHostedStyle - endpoint overrides emulator",
emulatorHost: "localhost:8000",
endpoint: &localhost9000,
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4,
Style: VirtualHostedStyle(),
Insecure: true,
},
want: "http://" + bucketName + ".localhost:9000/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=35e0b9d33901a2518956821175f88c2c4eb3f4461b725af74b37c36d23f8bbe927558ac57b0be40d345f20bca55ba0652d38b7a620f8da68d4f733706ad104da468c3a039459acf35f3022e388760cd49893c998c33fe3ccc8c022d7034ab98bdbdcac4b680bb24ae5ed586a42ee9495a873ffc484e297853a8a3892d0d6385c980cb7e3c5c8bdd4939b4c17105f10fe8b5b9744017bf59431ff176c1550ae1c64ddd6628096eb6895c97c5da4d850aca72c14b7f5018c15b34d4b00ec63ff2ccb688ddbef2d32648e247ffd0137498080f320f293eb811a94fb526227324bbbd01335446388797803e67d802f97b52565deba3d2387ecabf4f3094662236017" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "BucketBoundHostname - emulator",
emulatorHost: "localhost:8000",
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4,
Style: BucketBoundHostname("myhost"),
},
want: "https://" + "myhost/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=15fe19f6c61bcbdbd6473c32f2bec29caa8a5fa3b2ce32cfb5329a71edaa0d4e5ffe6469f32ed4c23ca2fbed3882fdf1ed107c6a98c2c4995dda6036c64bae51e6cb542c353618f483832aa1f3ef85342ddadd69c13ad4c69fd3f573ea5cf325a58056e3d5a37005217662af63b49fef8688de3c5c7a2f7b43651a030edd0813eb7f7713989a4c29a8add65133ce652895fea9de7dbc6248ee11b4d7c6c1e152df87700100e896e544ba8eeea96584078f56e699665140b750e90550b9b79633f4e7c8409efa807be5670d6e987eeee04a4180be9b9e30bb8557597beaf390a3805cc602c87a3e34800f8bc01449c3dd10ac2f2263e55e55b91e445052548d5e" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "BucketBoundHostname - endpoint",
endpoint: &localhost9000,
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4,
Style: BucketBoundHostname("myhost"),
},
want: "https://" + "myhost/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=15fe19f6c61bcbdbd6473c32f2bec29caa8a5fa3b2ce32cfb5329a71edaa0d4e5ffe6469f32ed4c23ca2fbed3882fdf1ed107c6a98c2c4995dda6036c64bae51e6cb542c353618f483832aa1f3ef85342ddadd69c13ad4c69fd3f573ea5cf325a58056e3d5a37005217662af63b49fef8688de3c5c7a2f7b43651a030edd0813eb7f7713989a4c29a8add65133ce652895fea9de7dbc6248ee11b4d7c6c1e152df87700100e896e544ba8eeea96584078f56e699665140b750e90550b9b79633f4e7c8409efa807be5670d6e987eeee04a4180be9b9e30bb8557597beaf390a3805cc602c87a3e34800f8bc01449c3dd10ac2f2263e55e55b91e445052548d5e" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "emulator host specifies scheme",
emulatorHost: "https://localhost:6000",
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4, //do v2 here
Insecure: true,
},
want: "http://localhost:6000/" + bucketName + "/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "endpoint specifies scheme",
endpoint: &localhost6000Https,
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV4,
Insecure: true,
},
want: "http://localhost:6000/" + bucketName + "/" + objectName +
"?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
"&X-Goog-Credential=xxx%40clientid%2F20021001%2Fauto%2Fstorage%2Fgoog4_request" +
"&X-Goog-Date=20021001T100000Z&X-Goog-Expires=86400" +
"&X-Goog-Signature=249c53142e57adf594b4f523a8a1f9c15f29b071e9abc0cf6665dbc5f692fc96fac4ab98bbea4c2397384367bc970a2e1771f2c86624475f3273970ecde8ff6df39d647e5c3f3263bf67a743e211c1958a96775edf53ece1f69ed337f0ab7fdc081c6c2b84e57b0922280d27f1da1bff47e77e3822fb1756e4c5cece9d220e6d0824ab9528e97e54f0cb09b352193b0e895344d894de11b3f5f9a2ec7d8fd6d0a4c487afd1896385a3ab9e8c3fcb3862ec0cad6ec10af1b574078eb7c79b558bcd85449a67079a0ee6da97fcbad074f1bf9fdfbdca12945336a8bd0a3b70b4c7708918cb83d10c7c4ff1f8b73275e9d1ba5d3db91069dffdf81eb7badf4e3c80" +
"&X-Goog-SignedHeaders=host",
},
{
desc: "emulator host specifies scheme using SigningSchemeV2",
emulatorHost: "https://localhost:8000",
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV2,
},
want: "https://localhost:8000/" + bucketName + "/" + objectName +
"?Expires=1033570800" +
"&GoogleAccessId=xxx%40clientid" +
"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
},
{
desc: "endpoint specifies scheme using SigningSchemeV2",
endpoint: &localhost6000Https,
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV2,
},
want: "https://localhost:6000/" + bucketName + "/" + objectName +
"?Expires=1033570800" +
"&GoogleAccessId=xxx%40clientid" +
"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
},
{
desc: "hostname in opts overrides all else",
endpoint: &localhost9000,
emulatorHost: "https://localhost:8000",
now: expires.Add(-24 * time.Hour),
opts: &SignedURLOptions{
GoogleAccessID: "xxx@clientid",
PrivateKey: dummyKey("rsa"),
Method: "POST",
Expires: expires,
Scheme: SigningSchemeV2,
Hostname: "localhost:6000",
},
want: "https://localhost:6000/" + bucketName + "/" + objectName +
"?Expires=1033570800" +
"&GoogleAccessId=xxx%40clientid" +
"&Signature=oRi3y2tBTmoDto7FezNx4AjC0RXA6fpJjTBa0hINeVroZ%2ByOeRU8MRwJbKg1IkBbV0IjtlPaGwv5YoUH16UYdipBjCXOS%2B1qgRWyzl8AnzvU%2BfwSXSlCk9zPtHHoBkFT7G4cZQOdDTLRrSG%2FmRJ3K09KEHYg%2Fc6R5Dd92inD1tLE2tiFMyHFs5uQHRMsepY4wrWiIQ4u53tPvk%2Fwiq1%2B9yL6x3QGblhdWwjX0BTVBOxexyKTlwczJW0XlWX8wpcTFfzQnJZuujbhanf2g9MGzSmkv3ylyuQdHMJDYp4Bzq%2FmnkNUg0Vp6iEvh9tyVdRNkwXeg3D8qn%2BFSOxcF%2B9vJw%3D%3D",
},
}
oldUTCNow := utcNow
defer func() {
utcNow = oldUTCNow
}()
for _, test := range tests {
t.Run(test.desc, func(s *testing.T) {
utcNow = func() time.Time {
return test.now
}
t.Setenv("STORAGE_EMULATOR_HOST", test.emulatorHost)
var opts []option.ClientOption
if test.endpoint != nil {
opts = append(opts, option.WithEndpoint(*test.endpoint), option.WithoutAuthentication())
}
c, err := NewClient(context.Background(), opts...)
if err != nil {
t.Fatalf("NewClient: %v", err)
}
got, err := c.Bucket(bucketName).SignedURL(objectName, test.opts)
if err != nil {
s.Fatal(err)
}
if got != test.want {
s.Fatalf("bucket.SidnedURL:\n\tgot:\t%v\n\twant:\t%v", got, test.want)
}
})
}
}