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 }