github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/s3/validate.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package s3
     7  
     8  import (
     9  	"fmt"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/aws/aws-sdk-go-v2/aws/arn"
    15  	"github.com/opentofu/opentofu/internal/tfdiags"
    16  	"github.com/zclconf/go-cty/cty"
    17  )
    18  
    19  const (
    20  	multiRegionKeyIdPattern = `mrk-[a-f0-9]{32}`
    21  	uuidRegexPattern        = `[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[ab89][a-f0-9]{3}-[a-f0-9]{12}`
    22  	aliasRegexPattern       = `alias/[a-zA-Z0-9/_-]+`
    23  )
    24  
    25  func validateKMSKey(path cty.Path, s string) (diags tfdiags.Diagnostics) {
    26  	if arn.IsARN(s) {
    27  		return validateKMSKeyARN(path, s)
    28  	}
    29  	return validateKMSKeyID(path, s)
    30  }
    31  
    32  func validateKMSKeyID(path cty.Path, s string) (diags tfdiags.Diagnostics) {
    33  	keyIdRegex := regexp.MustCompile(`^` + uuidRegexPattern + `|` + multiRegionKeyIdPattern + `|` + aliasRegexPattern + `$`)
    34  	if !keyIdRegex.MatchString(s) {
    35  		diags = diags.Append(tfdiags.AttributeValue(
    36  			tfdiags.Error,
    37  			"Invalid KMS Key ID",
    38  			fmt.Sprintf("Value must be a valid KMS Key ID, got %q", s),
    39  			path,
    40  		))
    41  		return diags
    42  	}
    43  
    44  	return diags
    45  }
    46  
    47  func validateKMSKeyARN(path cty.Path, s string) (diags tfdiags.Diagnostics) {
    48  	parsedARN, err := arn.Parse(s)
    49  	if err != nil {
    50  		diags = diags.Append(tfdiags.AttributeValue(
    51  			tfdiags.Error,
    52  			"Invalid KMS Key ARN",
    53  			fmt.Sprintf("Value must be a valid KMS Key ARN, got %q", s),
    54  			path,
    55  		))
    56  		return diags
    57  	}
    58  
    59  	if !isKeyARN(parsedARN) {
    60  		diags = diags.Append(tfdiags.AttributeValue(
    61  			tfdiags.Error,
    62  			"Invalid KMS Key ARN",
    63  			fmt.Sprintf("Value must be a valid KMS Key ARN, got %q", s),
    64  			path,
    65  		))
    66  		return diags
    67  	}
    68  
    69  	return diags
    70  }
    71  
    72  func validateNestedAssumeRole(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
    73  	var diags tfdiags.Diagnostics
    74  
    75  	if val, ok := stringAttrOk(obj, "role_arn"); !ok || val == "" {
    76  		path := objPath.GetAttr("role_arn")
    77  		diags = diags.Append(attributeErrDiag(
    78  			"Missing Required Value",
    79  			fmt.Sprintf("The attribute %q is required by the backend.\n\n", pathString(path))+
    80  				"Refer to the backend documentation for additional information which attributes are required.",
    81  			path,
    82  		))
    83  	}
    84  
    85  	if val, ok := stringAttrOk(obj, "duration"); ok {
    86  		validateDuration(val, 15*time.Minute, 12*time.Hour, objPath.GetAttr("duration"), &diags)
    87  	}
    88  
    89  	if val, ok := stringAttrOk(obj, "external_id"); ok {
    90  		validateNonEmptyString(val, objPath.GetAttr("external_id"), &diags)
    91  	}
    92  
    93  	if val, ok := stringAttrOk(obj, "policy"); ok {
    94  		validateNonEmptyString(val, objPath.GetAttr("policy"), &diags)
    95  	}
    96  
    97  	if val, ok := stringAttrOk(obj, "session_name"); ok {
    98  		validateNonEmptyString(val, objPath.GetAttr("session_name"), &diags)
    99  	}
   100  
   101  	if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok {
   102  		validatePolicyARNSlice(val, objPath.GetAttr("policy_arns"), &diags)
   103  	}
   104  
   105  	return diags
   106  }
   107  
   108  func validateAssumeRoleWithWebIdentity(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
   109  	var diags tfdiags.Diagnostics
   110  
   111  	validateAttributesConflict(
   112  		cty.GetAttrPath("web_identity_token"),
   113  		cty.GetAttrPath("web_identity_token_file"),
   114  	)(obj, objPath, &diags)
   115  
   116  	if val, ok := stringAttrOk(obj, "session_name"); ok {
   117  		validateNonEmptyString(val, objPath.GetAttr("session_name"), &diags)
   118  	}
   119  
   120  	if val, ok := stringAttrOk(obj, "policy"); ok {
   121  		validateNonEmptyString(val, objPath.GetAttr("policy"), &diags)
   122  	}
   123  
   124  	if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok {
   125  		validatePolicyARNSlice(val, objPath.GetAttr("policy_arns"), &diags)
   126  	}
   127  
   128  	if val, ok := stringAttrOk(obj, "duration"); ok {
   129  		validateDuration(val, 15*time.Minute, 12*time.Hour, objPath.GetAttr("duration"), &diags)
   130  	}
   131  
   132  	return diags
   133  }
   134  
   135  func isKeyARN(arn arn.ARN) bool {
   136  	return keyIdFromARNResource(arn.Resource) != "" || aliasIdFromARNResource(arn.Resource) != ""
   137  }
   138  
   139  func keyIdFromARNResource(s string) string {
   140  	keyIdResourceRegex := regexp.MustCompile(`^key/(` + uuidRegexPattern + `|` + multiRegionKeyIdPattern + `)$`)
   141  	matches := keyIdResourceRegex.FindStringSubmatch(s)
   142  	if matches == nil || len(matches) != 2 {
   143  		return ""
   144  	}
   145  
   146  	return matches[1]
   147  }
   148  
   149  func aliasIdFromARNResource(s string) string {
   150  	aliasIdResourceRegex := regexp.MustCompile(`^(` + aliasRegexPattern + `)$`)
   151  	matches := aliasIdResourceRegex.FindStringSubmatch(s)
   152  	if matches == nil || len(matches) != 2 {
   153  		return ""
   154  	}
   155  
   156  	return matches[1]
   157  }
   158  
   159  type objectValidator func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics)
   160  
   161  func validateAttributesConflict(paths ...cty.Path) objectValidator {
   162  	applyPath := func(obj cty.Value, path cty.Path) (cty.Value, error) {
   163  		if len(path) == 0 {
   164  			return cty.NilVal, nil
   165  		}
   166  		for _, step := range path {
   167  			val, err := step.Apply(obj)
   168  			if err != nil {
   169  				return cty.NilVal, err
   170  			}
   171  			if val.IsNull() {
   172  				return cty.NilVal, nil
   173  			}
   174  			obj = val
   175  		}
   176  		return obj, nil
   177  	}
   178  
   179  	return func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) {
   180  		found := false
   181  		for _, path := range paths {
   182  			val, err := applyPath(obj, path)
   183  			if err != nil {
   184  				*diags = diags.Append(attributeErrDiag(
   185  					"Invalid Path for Schema",
   186  					"The S3 Backend unexpectedly provided a path that does not match the schema. "+
   187  						"Please report this to the developers.\n\n"+
   188  						"Path: "+pathString(path)+"\n\n"+
   189  						"Error: "+err.Error(),
   190  					objPath,
   191  				))
   192  				continue
   193  			}
   194  			if !val.IsNull() {
   195  				if found {
   196  					pathStrs := make([]string, len(paths))
   197  					for i, path := range paths {
   198  						pathStrs[i] = pathString(path)
   199  					}
   200  					*diags = diags.Append(attributeErrDiag(
   201  						"Invalid Attribute Combination",
   202  						fmt.Sprintf(`Only one of %s can be set.`, strings.Join(pathStrs, ", ")),
   203  						objPath,
   204  					))
   205  					return
   206  				}
   207  				found = true
   208  			}
   209  		}
   210  	}
   211  }
   212  
   213  func attributeErrDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnostic {
   214  	return tfdiags.AttributeValue(tfdiags.Error, summary, detail, attrPath.Copy())
   215  }
   216  
   217  func attributeWarningDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnostic {
   218  	return tfdiags.AttributeValue(tfdiags.Warning, summary, detail, attrPath.Copy())
   219  }
   220  
   221  func validateNonEmptyString(val string, path cty.Path, diags *tfdiags.Diagnostics) {
   222  	if len(strings.TrimSpace(val)) == 0 {
   223  		*diags = diags.Append(attributeErrDiag(
   224  			"Invalid Value",
   225  			"The value cannot be empty or all whitespace",
   226  			path,
   227  		))
   228  	}
   229  }
   230  
   231  func validatePolicyARNSlice(val []string, path cty.Path, diags *tfdiags.Diagnostics) {
   232  	for _, v := range val {
   233  		arn, err := arn.Parse(v)
   234  		if err != nil {
   235  			*diags = diags.Append(attributeErrDiag(
   236  				"Invalid ARN",
   237  				fmt.Sprintf("The value %q cannot be parsed as an ARN: %s", val, err),
   238  				path,
   239  			))
   240  			break
   241  		} else {
   242  			if !strings.HasPrefix(arn.Resource, "policy/") {
   243  				*diags = diags.Append(attributeErrDiag(
   244  					"Invalid IAM Policy ARN",
   245  					fmt.Sprintf("Value must be a valid IAM Policy ARN, got %q", val),
   246  					path,
   247  				))
   248  			}
   249  		}
   250  	}
   251  }
   252  
   253  func validateDuration(val string, min, max time.Duration, path cty.Path, diags *tfdiags.Diagnostics) {
   254  	d, err := time.ParseDuration(val)
   255  	if err != nil {
   256  		*diags = diags.Append(attributeErrDiag(
   257  			"Invalid Duration",
   258  			fmt.Sprintf("The value %q cannot be parsed as a duration: %s", val, err),
   259  			path,
   260  		))
   261  		return
   262  	}
   263  	if (min > 0 && d < min) || (max > 0 && d > max) {
   264  		*diags = diags.Append(attributeErrDiag(
   265  			"Invalid Duration",
   266  			fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, val),
   267  			path,
   268  		))
   269  	}
   270  }