diff --git a/internal/backend/remote-state/s3/backend.go b/internal/backend/remote-state/s3/backend.go index 2474f19df1b2..042f5f6c144c 100644 --- a/internal/backend/remote-state/s3/backend.go +++ b/internal/backend/remote-state/s3/backend.go @@ -386,6 +386,7 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) Validators: []stringValidator{ validateStringNotEmpty, validateStringS3Path, + validateStringDoesNotContain("//"), }, } keyValidators.ValidateAttr(val, attrPath, &diags) diff --git a/internal/backend/remote-state/s3/backend_test.go b/internal/backend/remote-state/s3/backend_test.go index 228ba3e407fa..21cbc4e171b1 100644 --- a/internal/backend/remote-state/s3/backend_test.go +++ b/internal/backend/remote-state/s3/backend_test.go @@ -1137,6 +1137,20 @@ func TestBackendConfig_PrepareConfigValidation(t *testing.T) { ), }, }, + "key with double slash": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test/with/double//slash"), + "region": cty.StringVal("us-west-2"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `Value must not contain "//"`, + cty.GetAttrPath("key"), + ), + }, + }, "null region": { config: cty.ObjectVal(map[string]cty.Value{ diff --git a/internal/backend/remote-state/s3/validate.go b/internal/backend/remote-state/s3/validate.go index 00b0ae1a47d5..c094f1fc7c12 100644 --- a/internal/backend/remote-state/s3/validate.go +++ b/internal/backend/remote-state/s3/validate.go @@ -121,6 +121,18 @@ func validateStringMatches(re *regexp.Regexp, description string) stringValidato } } +func validateStringDoesNotContain(s string) stringValidator { + return func(val string, path cty.Path, diags *tfdiags.Diagnostics) { + if strings.Contains(val, s) { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + fmt.Sprintf(`Value must not contain "%s"`, s), + path, + )) + } + } +} + func validateStringInSlice(sl []string) stringValidator { return func(val string, path cty.Path, diags *tfdiags.Diagnostics) { match := false diff --git a/internal/backend/remote-state/s3/validate_test.go b/internal/backend/remote-state/s3/validate_test.go index b947d11aa799..4dad4f838cb8 100644 --- a/internal/backend/remote-state/s3/validate_test.go +++ b/internal/backend/remote-state/s3/validate_test.go @@ -751,3 +751,46 @@ func TestValidateStringURL(t *testing.T) { } } + +func Test_validateStringDoesNotContain(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + s string + expected tfdiags.Diagnostics + }{ + "valid": { + val: "foo", + s: "bar", + }, + + "invalid": { + val: "foobarbaz", + s: "bar", + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `Value must not contain "bar"`, + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var diags tfdiags.Diagnostics + validateStringDoesNotContain(testcase.s)(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +}