diff --git a/internal/backend/remote-state/s3/backend.go b/internal/backend/remote-state/s3/backend.go index 6b07070647..791576a723 100644 --- a/internal/backend/remote-state/s3/backend.go +++ b/internal/backend/remote-state/s3/backend.go @@ -279,6 +279,8 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) cty.Path{}, )) } + + diags = diags.Append(validateKMSKey(cty.Path{cty.GetAttrStep{Name: "kms_key_id"}}, val.AsString())) } if val := obj.GetAttr("workspace_key_prefix"); !val.IsNull() { diff --git a/internal/backend/remote-state/s3/testing.go b/internal/backend/remote-state/s3/testing.go new file mode 100644 index 0000000000..25c58625a2 --- /dev/null +++ b/internal/backend/remote-state/s3/testing.go @@ -0,0 +1,26 @@ +package s3 + +import ( + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// diagnosticComparer is a Comparer function for use with cmp.Diff to compare two tfdiags.Diagnostic values +func diagnosticComparer(l, r tfdiags.Diagnostic) bool { + if l.Severity() != r.Severity() { + return false + } + if l.Description() != r.Description() { + return false + } + + lp := tfdiags.GetAttribute(l) + rp := tfdiags.GetAttribute(r) + if len(lp) != len(rp) { + return false + } + if !lp.Equals(rp) { + return false + } + + return true +} diff --git a/internal/backend/remote-state/s3/validate.go b/internal/backend/remote-state/s3/validate.go new file mode 100644 index 0000000000..6f8c0a1a2a --- /dev/null +++ b/internal/backend/remote-state/s3/validate.go @@ -0,0 +1,80 @@ +package s3 + +import ( + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +const ( + multiRegionKeyIdPattern = `mrk-[a-f0-9]{32}` + uuidRegexPattern = `[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[ab89][a-f0-9]{3}-[a-f0-9]{12}` +) + +func validateKMSKey(path cty.Path, s string) (diags tfdiags.Diagnostics) { + if arn.IsARN(s) { + return validateKMSKeyARN(path, s) + } + return validateKMSKeyID(path, s) +} + +func validateKMSKeyID(path cty.Path, s string) (diags tfdiags.Diagnostics) { + keyIdRegex := regexp.MustCompile(`^` + uuidRegexPattern + `|` + multiRegionKeyIdPattern + `$`) + if !keyIdRegex.MatchString(s) { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ID", + fmt.Sprintf("Value must be a valid KMS Key ID, got %q", s), + path, + )) + return diags + } + + return diags +} + +func validateKMSKeyARN(path cty.Path, s string) (diags tfdiags.Diagnostics) { + if _, err := arn.Parse(s); err != nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + fmt.Sprintf("Value must be a valid KMS Key ARN, got %q", s), + path, + )) + return diags + } + + if !isKeyARN(s) { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + fmt.Sprintf("Value must be a valid KMS Key ARN, got %q", s), + path, + )) + return diags + } + + return diags +} + +func isKeyARN(s string) bool { + parsedARN, err := arn.Parse(s) + if err != nil { + return false + } + + return keyIdFromARNResource(parsedARN.Resource) != "" +} + +func keyIdFromARNResource(s string) string { + keyIdResourceRegex := regexp.MustCompile(`^key/(` + uuidRegexPattern + `|` + multiRegionKeyIdPattern + `)$`) + matches := keyIdResourceRegex.FindStringSubmatch(s) + if matches == nil || len(matches) != 2 { + return "" + } + + return matches[1] +} diff --git a/internal/backend/remote-state/s3/validate_test.go b/internal/backend/remote-state/s3/validate_test.go new file mode 100644 index 0000000000..a4d5e32ca3 --- /dev/null +++ b/internal/backend/remote-state/s3/validate_test.go @@ -0,0 +1,154 @@ +package s3 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +func TestValidateKMSKey(t *testing.T) { + t.Parallel() + + path := cty.Path{cty.GetAttrStep{Name: "field"}} + + testcases := map[string]struct { + in string + expected tfdiags.Diagnostics + }{ + "kms key id": { + in: "57ff7a43-341d-46b6-aee3-a450c9de6dc8", + }, + "kms key arn": { + in: "arn:aws:kms:us-west-2:111122223333:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8", + }, + "kms multi-region key id": { + in: "mrk-f827515944fb43f9b902a09d2c8b554f", + }, + "kms multi-region key arn": { + in: "arn:aws:kms:us-west-2:111122223333:key/mrk-a835af0b39c94b86a21a8fc9535df681", + }, + "kms key alias": { + in: "alias/arbitrary-key", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ID", + `Value must be a valid KMS Key ID, got "alias/arbitrary-key"`, + path, + ), + }, + }, + "kms key alias arn": { + in: "arn:aws:kms:us-west-2:111122223333:alias/arbitrary-key", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "arn:aws:kms:us-west-2:111122223333:alias/arbitrary-key"`, + path, + ), + }, + }, + "invalid key": { + in: "$%wrongkey", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ID", + `Value must be a valid KMS Key ID, got "$%wrongkey"`, + path, + ), + }, + }, + "non-kms arn": { + in: "arn:aws:lamda:foo:bar:key/xyz", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "arn:aws:lamda:foo:bar:key/xyz"`, + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := validateKMSKey(path, testcase.in) + + if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestValidateKeyARN(t *testing.T) { + t.Parallel() + + path := cty.Path{cty.GetAttrStep{Name: "field"}} + + testcases := map[string]struct { + in string + expected tfdiags.Diagnostics + }{ + "kms key id": { + in: "arn:aws:kms:us-west-2:123456789012:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8", + }, + "kms mrk key id": { + in: "arn:aws:kms:us-west-2:111122223333:key/mrk-a835af0b39c94b86a21a8fc9535df681", + }, + "kms non-key id": { + in: "arn:aws:kms:us-west-2:123456789012:something/else", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "arn:aws:kms:us-west-2:123456789012:something/else"`, + path, + ), + }, + }, + "non-kms arn": { + in: "arn:aws:iam::123456789012:user/David", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "arn:aws:iam::123456789012:user/David"`, + path, + ), + }, + }, + "not an arn": { + in: "not an arn", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "not an arn"`, + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := validateKMSKeyARN(path, testcase.in) + + if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +}