opentofu/internal/backend/remote-state/s3/validate.go
namgyalangmo cb2e9119aa
Update copyright notice (#1232)
Signed-off-by: namgyalangmo <75657887+namgyalangmo@users.noreply.github.com>
2024-02-08 09:48:59 +00:00

271 lines
7.2 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package s3
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/opentofu/opentofu/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}`
aliasRegexPattern = `alias/[a-zA-Z0-9/_-]+`
)
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 + `|` + aliasRegexPattern + `$`)
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) {
parsedARN, err := arn.Parse(s)
if 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(parsedARN) {
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 validateNestedAssumeRole(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if val, ok := stringAttrOk(obj, "role_arn"); !ok || val == "" {
path := objPath.GetAttr("role_arn")
diags = diags.Append(attributeErrDiag(
"Missing Required Value",
fmt.Sprintf("The attribute %q is required by the backend.\n\n", pathString(path))+
"Refer to the backend documentation for additional information which attributes are required.",
path,
))
}
if val, ok := stringAttrOk(obj, "duration"); ok {
validateDuration(val, 15*time.Minute, 12*time.Hour, objPath.GetAttr("duration"), &diags)
}
if val, ok := stringAttrOk(obj, "external_id"); ok {
validateNonEmptyString(val, objPath.GetAttr("external_id"), &diags)
}
if val, ok := stringAttrOk(obj, "policy"); ok {
validateNonEmptyString(val, objPath.GetAttr("policy"), &diags)
}
if val, ok := stringAttrOk(obj, "session_name"); ok {
validateNonEmptyString(val, objPath.GetAttr("session_name"), &diags)
}
if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok {
validatePolicyARNSlice(val, objPath.GetAttr("policy_arns"), &diags)
}
return diags
}
func validateAssumeRoleWithWebIdentity(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
validateAttributesConflict(
cty.GetAttrPath("web_identity_token"),
cty.GetAttrPath("web_identity_token_file"),
)(obj, objPath, &diags)
if val, ok := stringAttrOk(obj, "session_name"); ok {
validateNonEmptyString(val, objPath.GetAttr("session_name"), &diags)
}
if val, ok := stringAttrOk(obj, "policy"); ok {
validateNonEmptyString(val, objPath.GetAttr("policy"), &diags)
}
if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok {
validatePolicyARNSlice(val, objPath.GetAttr("policy_arns"), &diags)
}
if val, ok := stringAttrOk(obj, "duration"); ok {
validateDuration(val, 15*time.Minute, 12*time.Hour, objPath.GetAttr("duration"), &diags)
}
return diags
}
func isKeyARN(arn arn.ARN) bool {
return keyIdFromARNResource(arn.Resource) != "" || aliasIdFromARNResource(arn.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]
}
func aliasIdFromARNResource(s string) string {
aliasIdResourceRegex := regexp.MustCompile(`^(` + aliasRegexPattern + `)$`)
matches := aliasIdResourceRegex.FindStringSubmatch(s)
if matches == nil || len(matches) != 2 {
return ""
}
return matches[1]
}
type objectValidator func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics)
func validateAttributesConflict(paths ...cty.Path) objectValidator {
applyPath := func(obj cty.Value, path cty.Path) (cty.Value, error) {
if len(path) == 0 {
return cty.NilVal, nil
}
for _, step := range path {
val, err := step.Apply(obj)
if err != nil {
return cty.NilVal, err
}
if val.IsNull() {
return cty.NilVal, nil
}
obj = val
}
return obj, nil
}
return func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) {
found := false
for _, path := range paths {
val, err := applyPath(obj, path)
if err != nil {
*diags = diags.Append(attributeErrDiag(
"Invalid Path for Schema",
"The S3 Backend unexpectedly provided a path that does not match the schema. "+
"Please report this to the developers.\n\n"+
"Path: "+pathString(path)+"\n\n"+
"Error: "+err.Error(),
objPath,
))
continue
}
if !val.IsNull() {
if found {
pathStrs := make([]string, len(paths))
for i, path := range paths {
pathStrs[i] = pathString(path)
}
*diags = diags.Append(attributeErrDiag(
"Invalid Attribute Combination",
fmt.Sprintf(`Only one of %s can be set.`, strings.Join(pathStrs, ", ")),
objPath,
))
return
}
found = true
}
}
}
}
func attributeErrDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnostic {
return tfdiags.AttributeValue(tfdiags.Error, summary, detail, attrPath.Copy())
}
func attributeWarningDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnostic {
return tfdiags.AttributeValue(tfdiags.Warning, summary, detail, attrPath.Copy())
}
func validateNonEmptyString(val string, path cty.Path, diags *tfdiags.Diagnostics) {
if len(strings.TrimSpace(val)) == 0 {
*diags = diags.Append(attributeErrDiag(
"Invalid Value",
"The value cannot be empty or all whitespace",
path,
))
}
}
func validatePolicyARNSlice(val []string, path cty.Path, diags *tfdiags.Diagnostics) {
for _, v := range val {
arn, err := arn.Parse(v)
if err != nil {
*diags = diags.Append(attributeErrDiag(
"Invalid ARN",
fmt.Sprintf("The value %q cannot be parsed as an ARN: %s", val, err),
path,
))
break
} else {
if !strings.HasPrefix(arn.Resource, "policy/") {
*diags = diags.Append(attributeErrDiag(
"Invalid IAM Policy ARN",
fmt.Sprintf("Value must be a valid IAM Policy ARN, got %q", val),
path,
))
}
}
}
}
func validateDuration(val string, min, max time.Duration, path cty.Path, diags *tfdiags.Diagnostics) {
d, err := time.ParseDuration(val)
if err != nil {
*diags = diags.Append(attributeErrDiag(
"Invalid Duration",
fmt.Sprintf("The value %q cannot be parsed as a duration: %s", val, err),
path,
))
return
}
if (min > 0 && d < min) || (max > 0 && d > max) {
*diags = diags.Append(attributeErrDiag(
"Invalid Duration",
fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, val),
path,
))
}
}