Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: added gcs name validation #10426

Merged
merged 11 commits into from
Apr 15, 2024
2 changes: 2 additions & 0 deletions mmv1/products/storage/Bucket.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ properties:
- !ruby/object:Api::Type::String
name: 'name'
description: 'The name of the bucket'
validation: !ruby/object:Provider::Terraform::Validation
function: 'verify.ValidateGCSName'
- !ruby/object:Api::Type::NestedObject
name: 'owner'
description: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/hashicorp/terraform-provider-google/google/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
"github.com/hashicorp/terraform-provider-google/google/verify"

"github.com/gammazero/workerpool"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
Expand Down Expand Up @@ -62,6 +63,7 @@ func ResourceStorageBucket() *schema.Resource {
Required: true,
ForceNew: true,
Description: `The name of the bucket.`,
ValidateFunc: verify.ValidateGCSName,
},

"encryption": {
Expand Down Expand Up @@ -578,9 +580,6 @@ func resourceStorageBucketCreate(d *schema.ResourceData, meta interface{}) error

// Get the bucket and location
bucket := d.Get("name").(string)
if err := tpgresource.CheckGCSName(bucket); err != nil {
return err
}
location := d.Get("location").(string)

// Create a bucket, setting the labels, location and name.
Expand Down
25 changes: 0 additions & 25 deletions mmv1/third_party/terraform/tpgresource/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,31 +615,6 @@ func Fake404(reasonResourceType, resourceName string) *googleapi.Error {
}
}

// validate name of the gcs bucket. Guidelines are located at https://cloud.google.com/storage/docs/naming-buckets
// this does not attempt to check for IP addresses or close misspellings of "google"
func CheckGCSName(name string) error {
if strings.HasPrefix(name, "goog") {
return fmt.Errorf("error: bucket name %s cannot start with %q", name, "goog")
}

if strings.Contains(name, "google") {
return fmt.Errorf("error: bucket name %s cannot contain %q", name, "google")
}

valid, _ := regexp.MatchString("^[a-z0-9][a-z0-9_.-]{1,220}[a-z0-9]$", name)
if !valid {
return fmt.Errorf("error: bucket name validation failed %v. See https://cloud.google.com/storage/docs/naming-buckets", name)
}

for _, str := range strings.Split(name, ".") {
valid, _ := regexp.MatchString("^[a-z0-9_-]{1,63}$", str)
if !valid {
return fmt.Errorf("error: bucket name validation failed %v", str)
}
}
return nil
}

// CheckGoogleIamPolicy makes assertions about the contents of a google_iam_policy data source's policy_data attribute
func CheckGoogleIamPolicy(value string) error {
if strings.Contains(value, "\"description\":\"\"") {
Expand Down
42 changes: 0 additions & 42 deletions mmv1/third_party/terraform/tpgresource/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/hashicorp/terraform-provider-google/google/acctest"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"

Expand Down Expand Up @@ -1254,44 +1253,3 @@ func TestReplaceVars(t *testing.T) {
})
}
}

func TestCheckGCSName(t *testing.T) {
valid63 := acctest.RandString(t, 63)
cases := map[string]bool{
// Valid
"foobar": true,
"foobar1": true,
"12345": true,
"foo_bar_baz": true,
"foo-bar-baz": true,
"foo-bar_baz1": true,
"foo--bar": true,
"foo__bar": true,
"foo-goog": true,
"foo.goog": true,
valid63: true,
fmt.Sprintf("%s.%s.%s", valid63, valid63, valid63): true,

// Invalid
"goog-foobar": false,
"foobar-google": false,
"-foobar": false,
"foobar-": false,
"_foobar": false,
"foobar_": false,
"fo": false,
"foo$bar": false,
"foo..bar": false,
acctest.RandString(t, 64): false,
fmt.Sprintf("%s.%s.%s.%s", valid63, valid63, valid63, valid63): false,
}

for bucketName, valid := range cases {
err := tpgresource.CheckGCSName(bucketName)
if valid && err != nil {
t.Errorf("The bucket name %s was expected to pass validation and did not pass.", bucketName)
} else if !valid && err == nil {
t.Errorf("The bucket name %s was NOT expected to pass validation and passed.", bucketName)
}
}
}
48 changes: 48 additions & 0 deletions mmv1/third_party/terraform/verify/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ var (
Rfc6996Asn32BitMin = int64(4200000000)
Rfc6996Asn32BitMax = int64(4294967294)
GcpRouterPartnerAsn = int64(16550)

// Format of GCS Bucket Name
// https://cloud.google.com/storage/docs/naming-buckets
GCSNameValidChars = "^[a-z0-9_.-]*$"
GCSNameStartEndChars = "^[a-z|0-9].*[a-z|0-9]$"
GCSNameLength = "^.{3,222}"
GCSNameLengthSplit = "^.{1,63}$"
GCSNameCidr = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$"
GCSNameGoogPrefix = "^goog.*$"
GCSNameContainsGoogle = "^.*google.*$"
)

var Rfc1918Networks = []string{
Expand All @@ -89,6 +99,44 @@ func ValidateGCEName(v interface{}, k string) (ws []string, errors []error) {
return ValidateRegexp(re)(v, k)
}

// validateGCSName ensures the name of a gcs bucket matches the requirements for GCS Buckets
// https://cloud.google.com/storage/docs/naming-buckets
func ValidateGCSName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)

if !regexp.MustCompile(GCSNameValidChars).MatchString(value) {
errors = append(errors, fmt.Errorf("%q name value can only contain lowercase letters, numeric characters, dashes (-), underscores (_), and dots (.)", value))
}

if !regexp.MustCompile(GCSNameStartEndChars).MatchString(value) {
errors = append(errors, fmt.Errorf("%q name value must start and end with a number or letter", value))
}

if !regexp.MustCompile(GCSNameLength).MatchString(value) {
errors = append(errors, fmt.Errorf("%q name value must contain 3-63 characters. Names containing dots can contain up to 222 characters, but each dot-separated component can be no longer than 63 characters", value))
}

for _, str := range strings.Split(value, ".") {
if !regexp.MustCompile(GCSNameLengthSplit).MatchString(str) {
errors = append(errors, fmt.Errorf("%q name value must contain 3-63 characters. Names containing dots can contain up to 222 characters, but each dot-separated component can be no longer than 63 characters", value))
}
}

if regexp.MustCompile(GCSNameCidr).MatchString(value) {
errors = append(errors, fmt.Errorf("%q name value cannot be represented as an IP address in dotted-decimal notation (for example, 192.168.5.4)", value))
}

if regexp.MustCompile(GCSNameGoogPrefix).MatchString(value) {
errors = append(errors, fmt.Errorf("%q name value cannot begin with the \"goog\" prefix", value))
}

if regexp.MustCompile(GCSNameContainsGoogle).MatchString(strings.ReplaceAll(value, "0", "o")) {
errors = append(errors, fmt.Errorf("%q name value cannot contain \"google\" or close misspellings, such as \"g00gle\"", value))
}

return
}

// Ensure that the BGP ASN value of Cloud Router is a valid value as per RFC6996 or a value of 16550
func ValidateRFC6996Asn(v interface{}, k string) (ws []string, errors []error) {
value := int64(v.(int))
Expand Down
40 changes: 40 additions & 0 deletions mmv1/third_party/terraform/verify/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,43 @@ func TestValidateIAMCustomRoleIDRegex(t *testing.T) {
t.Errorf("Failed to validate IAMCustomRole IDs: %v", es)
}
}

func TestValidateGCSName(t *testing.T) {
x := []StringValidationTestCase{
// No errors
{TestName: "basic", Value: "foobar"},
{TestName: "has number", Value: "foobar1"},
{TestName: "all numbers", Value: "12345"},
{TestName: "all _", Value: "foo_bar_baz"},
{TestName: "all -", Value: "foo-bar-baz"},
{TestName: "begins with number", Value: "1foo-bar_baz"},
{TestName: "ends with number", Value: "foo-bar_baz1"},
{TestName: "almost an ip", Value: "192.168.5.foo"},
{TestName: "has _", Value: "foo-bar_baz"},
{TestName: "--", Value: "foo--bar"},
{TestName: "__", Value: "foo__bar"},
{TestName: "-goog", Value: "foo-goog"},
{TestName: ".goog", Value: "foo.goog"},

// With errors
{TestName: "invalid char $", Value: "foo$bar", ExpectError: true},
{TestName: "has uppercase", Value: "fooBar", ExpectError: true},
{TestName: "begins with -", Value: "-foobar", ExpectError: true},
{TestName: "ends with -", Value: "foobar-", ExpectError: true},
{TestName: "begins with _", Value: "_foobar", ExpectError: true},
{TestName: "ends with _", Value: "foobar_", ExpectError: true},
{TestName: "less than 3 chars", Value: "fo", ExpectError: true},
{TestName: "..", Value: "foo..bar", ExpectError: true},
{TestName: "greater than 63 chars with no .", Value: "my-really-long-bucket-name-with-invalid-that-does-not-contain-a-period", ExpectError: true},
{TestName: "greater than 63 chars between .", Value: "my.really-long-bucket-name-with-invalid-that-does-contain-a-period-but.is-too-long", ExpectError: true},
{TestName: "has goog prefix", Value: "goog-foobar", ExpectError: true},
{TestName: "almost an ip", Value: "192.168.5.1", ExpectError: true},
{TestName: "contains google", Value: "foobar-google", ExpectError: true},
{TestName: "contains close misspelling of google", Value: "foo-go0gle-bar", ExpectError: true},
}

es := TestStringValidationCases(x, ValidateGCSName)
if len(es) > 0 {
t.Errorf("Failed to validate GCS names: %v", es)
}
}
Loading