// 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, )) } }