| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| package verify |
| |
| import ( |
| "encoding/base64" |
| "fmt" |
| "net" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" |
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" |
| ) |
| |
| const ( |
| // Copied from the official Google Cloud auto-generated client. |
| ProjectRegex = "(?:(?:[-a-z0-9]{1,63}\\.)*(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?):)?(?:[0-9]{1,19}|(?:[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?))" |
| ProjectRegexWildCard = "(?:(?:[-a-z0-9]{1,63}\\.)*(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?):)?(?:[0-9]{1,19}|(?:[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?)|-)" |
| RegionRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?" |
| SubnetworkRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?" |
| |
| SubnetworkLinkRegex = "projects/(" + ProjectRegex + ")/regions/(" + RegionRegex + ")/subnetworks/(" + SubnetworkRegex + ")$" |
| |
| RFC1035NameTemplate = "[a-z](?:[-a-z0-9]{%d,%d}[a-z0-9])" |
| CloudIoTIdRegex = "^[a-zA-Z][-a-zA-Z0-9._+~%]{2,254}$" |
| |
| // Format of default Compute service accounts created by Google |
| // ${PROJECT_ID}-compute@developer.gserviceaccount.com where PROJECT_ID is an int64 (max 20 digits) |
| ComputeServiceAccountNameRegex = "[0-9]{1,20}-compute@developer.gserviceaccount.com" |
| |
| // https://cloud.google.com/iam/docs/understanding-custom-roles#naming_the_role |
| IAMCustomRoleIDRegex = "^[a-zA-Z0-9_\\.]{3,64}$" |
| |
| // https://cloud.google.com/managed-microsoft-ad/reference/rest/v1/projects.locations.global.domains/create#query-parameters |
| ADDomainNameRegex = "^[a-z][a-z0-9-]{0,14}\\.[a-z0-9-\\.]*[a-z]+[a-z0-9]*$" |
| ) |
| |
| var ( |
| // Service account name must have a length between 6 and 30. |
| // The first and last characters have different restrictions, than |
| // the middle characters. The middle characters length must be between |
| // 4 and 28 since the first and last character are excluded. |
| ServiceAccountNameRegex = fmt.Sprintf(RFC1035NameTemplate, 4, 28) |
| |
| ServiceAccountLinkRegexPrefix = "projects/" + ProjectRegexWildCard + "/serviceAccounts/" |
| PossibleServiceAccountNames = []string{ |
| ServiceDefaultAccountNameRegex, |
| ComputeServiceAccountNameRegex, |
| CreatedServiceAccountNameRegex, |
| } |
| ServiceAccountLinkRegex = ServiceAccountLinkRegexPrefix + "(" + strings.Join(PossibleServiceAccountNames, "|") + ")" |
| |
| ServiceAccountKeyNameRegex = ServiceAccountLinkRegexPrefix + "(.+)/keys/(.+)" |
| |
| // Format of service accounts created through the API |
| CreatedServiceAccountNameRegex = fmt.Sprintf(RFC1035NameTemplate, 4, 28) + "@" + ProjectNameInDNSFormRegex + "\\.iam\\.gserviceaccount\\.com$" |
| |
| // Format of service-created service account |
| // examples are: |
| // $PROJECTID@cloudbuild.gserviceaccount.com |
| // $PROJECTID@cloudservices.gserviceaccount.com |
| // $PROJECTID@appspot.gserviceaccount.com |
| ServiceDefaultAccountNameRegex = ProjectRegex + "@[a-z]+.gserviceaccount.com$" |
| |
| ProjectNameInDNSFormRegex = "[-a-z0-9\\.]{1,63}" |
| ProjectNameRegex = "^[A-Za-z0-9-'\"\\s!]{4,30}$" |
| |
| // Valid range for Cloud Router ASN values as per RFC6996 |
| // https://tools.ietf.org/html/rfc6996 |
| // Must be explicitly int64 to avoid overflow when building Terraform for 32bit architectures |
| Rfc6996Asn16BitMin = int64(64512) |
| Rfc6996Asn16BitMax = int64(65534) |
| 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{ |
| "10.0.0.0/8", |
| "172.16.0.0/12", |
| "192.168.0.0/16", |
| } |
| |
| // validateGCEName ensures that a field matches the requirements for Compute Engine resource names |
| // https://cloud.google.com/compute/docs/naming-resources#resource-name-format |
| func ValidateGCEName(v interface{}, k string) (ws []string, errors []error) { |
| re := `^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$` |
| 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)) |
| if !(value >= Rfc6996Asn16BitMin && value <= Rfc6996Asn16BitMax) && |
| !(value >= Rfc6996Asn32BitMin && value <= Rfc6996Asn32BitMax) && |
| value != GcpRouterPartnerAsn { |
| errors = append(errors, fmt.Errorf(`expected %q to be a RFC6996-compliant Local ASN: |
| must be either in the private ASN ranges: [64512..65534], [4200000000..4294967294]; |
| or be the value of [%d], got %d`, k, GcpRouterPartnerAsn, value)) |
| } |
| return |
| } |
| |
| func ValidateEnum(values []string) schema.SchemaValidateFunc { |
| return validation.StringInSlice(values, false) |
| } |
| |
| func ValidateRFC1918Network(min, max int) schema.SchemaValidateFunc { |
| return func(i interface{}, k string) (s []string, es []error) { |
| |
| s, es = validation.IsCIDRNetwork(min, max)(i, k) |
| if len(es) > 0 { |
| return |
| } |
| |
| v, _ := i.(string) |
| ip, _, _ := net.ParseCIDR(v) |
| for _, c := range Rfc1918Networks { |
| if _, ipnet, _ := net.ParseCIDR(c); ipnet.Contains(ip) { |
| return |
| } |
| } |
| |
| es = append(es, fmt.Errorf("expected %q to be an RFC1918-compliant CIDR, got: %s", k, v)) |
| |
| return |
| } |
| } |
| |
| func ValidateRFC3339Time(v interface{}, k string) (warnings []string, errors []error) { |
| time := v.(string) |
| if len(time) != 5 || time[2] != ':' { |
| errors = append(errors, fmt.Errorf("%q (%q) must be in the format HH:mm (RFC3339)", k, time)) |
| return |
| } |
| if hour, err := strconv.ParseUint(time[:2], 10, 0); err != nil || hour > 23 { |
| errors = append(errors, fmt.Errorf("%q (%q) does not contain a valid hour (00-23)", k, time)) |
| return |
| } |
| if min, err := strconv.ParseUint(time[3:], 10, 0); err != nil || min > 59 { |
| errors = append(errors, fmt.Errorf("%q (%q) does not contain a valid minute (00-59)", k, time)) |
| return |
| } |
| return |
| } |
| |
| func ValidateRFC1035Name(min, max int) schema.SchemaValidateFunc { |
| if min < 2 || max < min { |
| return func(i interface{}, k string) (s []string, errors []error) { |
| if min < 2 { |
| errors = append(errors, fmt.Errorf("min must be at least 2. Got: %d", min)) |
| } |
| if max < min { |
| errors = append(errors, fmt.Errorf("max must greater than min. Got [%d, %d]", min, max)) |
| } |
| return |
| } |
| } |
| |
| return ValidateRegexp(fmt.Sprintf("^"+RFC1035NameTemplate+"$", min-2, max-2)) |
| } |
| |
| func ValidateIpCidrRange(v interface{}, k string) (warnings []string, errors []error) { |
| _, _, err := net.ParseCIDR(v.(string)) |
| if err != nil { |
| errors = append(errors, fmt.Errorf("%q is not a valid IP CIDR range: %s", k, err)) |
| } |
| return |
| } |
| |
| func ValidateIAMCustomRoleID(v interface{}, k string) (warnings []string, errors []error) { |
| value := v.(string) |
| if !regexp.MustCompile(IAMCustomRoleIDRegex).MatchString(value) { |
| errors = append(errors, fmt.Errorf( |
| "%q (%q) doesn't match regexp %q", k, value, IAMCustomRoleIDRegex)) |
| } |
| return |
| } |
| |
| func OrEmpty(f schema.SchemaValidateFunc) schema.SchemaValidateFunc { |
| return func(i interface{}, k string) ([]string, []error) { |
| v, ok := i.(string) |
| if ok && v == "" { |
| return nil, nil |
| } |
| return f(i, k) |
| } |
| } |
| |
| func ValidateProjectID() schema.SchemaValidateFunc { |
| return func(v interface{}, k string) (ws []string, errors []error) { |
| value := v.(string) |
| |
| if !regexp.MustCompile("^" + ProjectRegex + "$").MatchString(value) { |
| errors = append(errors, fmt.Errorf( |
| "%q project_id must be 6 to 30 with lowercase letters, digits, hyphens and start with a letter. Trailing hyphens are prohibited.", value)) |
| } |
| return |
| } |
| } |
| |
| func ValidateDSProjectID() schema.SchemaValidateFunc { |
| return func(v interface{}, k string) (ws []string, errors []error) { |
| value := v.(string) |
| ids := strings.Split(value, "/") |
| value = ids[len(ids)-1] |
| |
| if !regexp.MustCompile("^" + ProjectRegex + "$").MatchString(value) { |
| errors = append(errors, fmt.Errorf( |
| "%q project_id must be 6 to 30 with lowercase letters, digits, hyphens and start with a letter. Trailing hyphens are prohibited.", value)) |
| } |
| return |
| } |
| } |
| |
| func ValidateProjectName() schema.SchemaValidateFunc { |
| return func(v interface{}, k string) (ws []string, errors []error) { |
| value := v.(string) |
| |
| if !regexp.MustCompile(ProjectNameRegex).MatchString(value) { |
| errors = append(errors, fmt.Errorf( |
| "%q name must be 4 to 30 characters with lowercase and uppercase letters, numbers, hyphen, single-quote, double-quote, space, and exclamation point.", value)) |
| } |
| return |
| } |
| } |
| |
| func ValidateDuration() schema.SchemaValidateFunc { |
| return func(i interface{}, k string) (s []string, es []error) { |
| v, ok := i.(string) |
| if !ok { |
| es = append(es, fmt.Errorf("expected type of %s to be string", k)) |
| return |
| } |
| |
| if _, err := time.ParseDuration(v); err != nil { |
| es = append(es, fmt.Errorf("expected %s to be a duration, but parsing gave an error: %s", k, err.Error())) |
| return |
| } |
| |
| return |
| } |
| } |
| |
| func ValidateNonNegativeDuration() schema.SchemaValidateFunc { |
| return func(i interface{}, k string) (s []string, es []error) { |
| v, ok := i.(string) |
| if !ok { |
| es = append(es, fmt.Errorf("expected type of %s to be string", k)) |
| return |
| } |
| |
| dur, err := time.ParseDuration(v) |
| if err != nil { |
| es = append(es, fmt.Errorf("expected %s to be a duration, but parsing gave an error: %s", k, err.Error())) |
| return |
| } |
| |
| if dur < 0 { |
| es = append(es, fmt.Errorf("duration %v must be a non-negative duration", dur)) |
| return |
| } |
| |
| return |
| } |
| } |
| |
| func ValidateIpAddress(i interface{}, val string) ([]string, []error) { |
| ip := net.ParseIP(i.(string)) |
| if ip == nil { |
| return nil, []error{fmt.Errorf("could not parse %q to IP address", val)} |
| } |
| return nil, nil |
| } |
| |
| func ValidateBase64String(i interface{}, val string) ([]string, []error) { |
| _, err := base64.StdEncoding.DecodeString(i.(string)) |
| if err != nil { |
| return nil, []error{fmt.Errorf("could not decode %q as a valid base64 value. Please use the terraform base64 functions such as base64encode() or filebase64() to supply a valid base64 string", val)} |
| } |
| return nil, nil |
| } |
| |
| // StringNotInSlice returns a SchemaValidateFunc which tests if the provided value |
| // is of type string and that it matches none of the element in the invalid slice. |
| // if ignorecase is true, case is ignored. |
| func StringNotInSlice(invalid []string, ignoreCase bool) schema.SchemaValidateFunc { |
| return func(i interface{}, k string) (s []string, es []error) { |
| v, ok := i.(string) |
| if !ok { |
| es = append(es, fmt.Errorf("expected type of %s to be string", k)) |
| return |
| } |
| |
| for _, str := range invalid { |
| if v == str || (ignoreCase && strings.ToLower(v) == strings.ToLower(str)) { |
| es = append(es, fmt.Errorf("expected %s to not match any of %v, got %s", k, invalid, v)) |
| return |
| } |
| } |
| |
| return |
| } |
| } |
| |
| // Ensure that hourly timestamp strings "HH:MM" have the minutes zeroed out for hourly only inputs |
| func ValidateHourlyOnly(val interface{}, key string) (warns []string, errs []error) { |
| v := val.(string) |
| parts := strings.Split(v, ":") |
| if len(parts) != 2 { |
| errs = append(errs, fmt.Errorf("%q must be in the format HH:00, got: %s", key, v)) |
| return |
| } |
| if parts[1] != "00" { |
| errs = append(errs, fmt.Errorf("%q does not allow minutes, it must be in the format HH:00, got: %s", key, v)) |
| } |
| i, err := strconv.Atoi(parts[0]) |
| if err != nil { |
| errs = append(errs, fmt.Errorf("%q cannot be parsed, it must be in the format HH:00, got: %s", key, v)) |
| } else if i < 0 || i > 23 { |
| errs = append(errs, fmt.Errorf("%q does not specify a valid hour, it must be in the format HH:00 where HH : [00-23], got: %s", key, v)) |
| } |
| return |
| } |
| |
| func ValidateRFC3339Date(v interface{}, k string) (warnings []string, errors []error) { |
| _, err := time.Parse(time.RFC3339, v.(string)) |
| if err != nil { |
| errors = append(errors, err) |
| } |
| return |
| } |
| |
| func ValidateADDomainName() schema.SchemaValidateFunc { |
| return func(v interface{}, k string) (ws []string, errors []error) { |
| value := v.(string) |
| |
| if len(value) > 64 || !regexp.MustCompile(ADDomainNameRegex).MatchString(value) { |
| errors = append(errors, fmt.Errorf( |
| "%q (%q) doesn't match regexp %q, domain_name must be 2 to 64 with lowercase letters, digits, hyphens, dots and start with a letter", k, value, ADDomainNameRegex)) |
| } |
| return |
| } |
| } |
| |
| func TestStringValidationCases(cases []StringValidationTestCase, validationFunc schema.SchemaValidateFunc) []error { |
| es := make([]error, 0) |
| for _, c := range cases { |
| es = append(es, TestStringValidation(c, validationFunc)...) |
| } |
| |
| return es |
| } |
| |
| func TestStringValidation(testCase StringValidationTestCase, validationFunc schema.SchemaValidateFunc) []error { |
| _, es := validationFunc(testCase.Value, testCase.TestName) |
| if testCase.ExpectError { |
| if len(es) > 0 { |
| return nil |
| } else { |
| return []error{fmt.Errorf("Didn't see expected error in case \"%s\" with string \"%s\"", testCase.TestName, testCase.Value)} |
| } |
| } |
| |
| return es |
| } |
| |
| type StringValidationTestCase struct { |
| TestName string |
| Value string |
| ExpectError bool |
| } |
| |
| func ValidateRegexp(re string) schema.SchemaValidateFunc { |
| return func(v interface{}, k string) (ws []string, errors []error) { |
| value := v.(string) |
| if !regexp.MustCompile(re).MatchString(value) { |
| errors = append(errors, fmt.Errorf( |
| "%q (%q) doesn't match regexp %q", k, value, re)) |
| } |
| |
| return |
| } |
| } |