github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/s3/validate_test.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 "testing" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/opentofu/opentofu/internal/tfdiags" 13 "github.com/zclconf/go-cty/cty" 14 ) 15 16 func TestValidateKMSKey(t *testing.T) { 17 t.Parallel() 18 19 path := cty.Path{cty.GetAttrStep{Name: "field"}} 20 21 testcases := map[string]struct { 22 in string 23 expected tfdiags.Diagnostics 24 }{ 25 "kms key id": { 26 in: "57ff7a43-341d-46b6-aee3-a450c9de6dc8", 27 }, 28 "kms key arn": { 29 in: "arn:aws:kms:us-west-2:111122223333:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8", 30 }, 31 "kms multi-region key id": { 32 in: "mrk-f827515944fb43f9b902a09d2c8b554f", 33 }, 34 "kms multi-region key arn": { 35 in: "arn:aws:kms:us-west-2:111122223333:key/mrk-a835af0b39c94b86a21a8fc9535df681", 36 }, 37 "kms key alias": { 38 in: "alias/arbitrary-key", 39 }, 40 "kms key alias arn": { 41 in: "arn:aws:kms:us-west-2:111122223333:alias/arbitrary-key", 42 }, 43 "invalid key": { 44 in: "$%wrongkey", 45 expected: tfdiags.Diagnostics{ 46 tfdiags.AttributeValue( 47 tfdiags.Error, 48 "Invalid KMS Key ID", 49 `Value must be a valid KMS Key ID, got "$%wrongkey"`, 50 path, 51 ), 52 }, 53 }, 54 "non-kms arn": { 55 in: "arn:aws:lamda:foo:bar:key/xyz", 56 expected: tfdiags.Diagnostics{ 57 tfdiags.AttributeValue( 58 tfdiags.Error, 59 "Invalid KMS Key ARN", 60 `Value must be a valid KMS Key ARN, got "arn:aws:lamda:foo:bar:key/xyz"`, 61 path, 62 ), 63 }, 64 }, 65 } 66 67 for name, testcase := range testcases { 68 testcase := testcase 69 t.Run(name, func(t *testing.T) { 70 t.Parallel() 71 72 diags := validateKMSKey(path, testcase.in) 73 74 if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" { 75 t.Errorf("unexpected diagnostics difference: %s", diff) 76 } 77 }) 78 } 79 } 80 81 func TestValidateKeyARN(t *testing.T) { 82 t.Parallel() 83 84 path := cty.Path{cty.GetAttrStep{Name: "field"}} 85 86 testcases := map[string]struct { 87 in string 88 expected tfdiags.Diagnostics 89 }{ 90 "kms key id": { 91 in: "arn:aws:kms:us-west-2:123456789012:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8", 92 }, 93 "kms mrk key id": { 94 in: "arn:aws:kms:us-west-2:111122223333:key/mrk-a835af0b39c94b86a21a8fc9535df681", 95 }, 96 "kms non-key id": { 97 in: "arn:aws:kms:us-west-2:123456789012:something/else", 98 expected: tfdiags.Diagnostics{ 99 tfdiags.AttributeValue( 100 tfdiags.Error, 101 "Invalid KMS Key ARN", 102 `Value must be a valid KMS Key ARN, got "arn:aws:kms:us-west-2:123456789012:something/else"`, 103 path, 104 ), 105 }, 106 }, 107 "non-kms arn": { 108 in: "arn:aws:iam::123456789012:user/David", 109 expected: tfdiags.Diagnostics{ 110 tfdiags.AttributeValue( 111 tfdiags.Error, 112 "Invalid KMS Key ARN", 113 `Value must be a valid KMS Key ARN, got "arn:aws:iam::123456789012:user/David"`, 114 path, 115 ), 116 }, 117 }, 118 "not an arn": { 119 in: "not an arn", 120 expected: tfdiags.Diagnostics{ 121 tfdiags.AttributeValue( 122 tfdiags.Error, 123 "Invalid KMS Key ARN", 124 `Value must be a valid KMS Key ARN, got "not an arn"`, 125 path, 126 ), 127 }, 128 }, 129 } 130 131 for name, testcase := range testcases { 132 testcase := testcase 133 t.Run(name, func(t *testing.T) { 134 t.Parallel() 135 136 diags := validateKMSKeyARN(path, testcase.in) 137 138 if diff := cmp.Diff(diags, testcase.expected, cmp.Comparer(diagnosticComparer)); diff != "" { 139 t.Errorf("unexpected diagnostics difference: %s", diff) 140 } 141 }) 142 } 143 } 144 145 func Test_validateAttributesConflict(t *testing.T) { 146 tests := []struct { 147 name string 148 paths []cty.Path 149 objValues map[string]cty.Value 150 expectErr bool 151 }{ 152 { 153 name: "Conflict Found", 154 paths: []cty.Path{ 155 {cty.GetAttrStep{Name: "attr1"}}, 156 {cty.GetAttrStep{Name: "attr2"}}, 157 }, 158 objValues: map[string]cty.Value{ 159 "attr1": cty.StringVal("value1"), 160 "attr2": cty.StringVal("value2"), 161 "attr3": cty.StringVal("value3"), 162 }, 163 expectErr: true, 164 }, 165 { 166 name: "No Conflict", 167 paths: []cty.Path{ 168 {cty.GetAttrStep{Name: "attr1"}}, 169 {cty.GetAttrStep{Name: "attr2"}}, 170 }, 171 objValues: map[string]cty.Value{ 172 "attr1": cty.StringVal("value1"), 173 "attr2": cty.NilVal, 174 "attr3": cty.StringVal("value3"), 175 }, 176 expectErr: false, 177 }, 178 { 179 name: "Nested: Conflict Found", 180 paths: []cty.Path{ 181 (cty.Path{cty.GetAttrStep{Name: "nested"}}).GetAttr("attr1"), 182 {cty.GetAttrStep{Name: "attr2"}}, 183 }, 184 objValues: map[string]cty.Value{ 185 "nested": cty.ObjectVal(map[string]cty.Value{ 186 "attr1": cty.StringVal("value1"), 187 }), 188 "attr2": cty.StringVal("value2"), 189 "attr3": cty.StringVal("value3"), 190 }, 191 expectErr: true, 192 }, 193 { 194 name: "Nested: No Conflict", 195 paths: []cty.Path{ 196 (cty.Path{cty.GetAttrStep{Name: "nested"}}).GetAttr("attr1"), 197 {cty.GetAttrStep{Name: "attr3"}}, 198 }, 199 objValues: map[string]cty.Value{ 200 "nested": cty.NilVal, 201 "attr1": cty.StringVal("value1"), 202 "attr3": cty.StringVal("value3"), 203 }, 204 expectErr: false, 205 }, 206 } 207 208 for _, test := range tests { 209 t.Run(test.name, func(t *testing.T) { 210 var diags tfdiags.Diagnostics 211 212 validator := validateAttributesConflict(test.paths...) 213 214 obj := cty.ObjectVal(test.objValues) 215 216 validator(obj, cty.Path{}, &diags) 217 218 if test.expectErr { 219 if !diags.HasErrors() { 220 t.Error("Expected validation errors, but got none.") 221 } 222 } else { 223 if diags.HasErrors() { 224 t.Errorf("Expected no errors, but got %s.", diags.Err()) 225 } 226 } 227 }) 228 } 229 } 230 231 func Test_validateNestedAssumeRole(t *testing.T) { 232 tests := []struct { 233 description string 234 input cty.Value 235 expectedDiags []string 236 }{ 237 { 238 description: "Valid Input", 239 input: cty.ObjectVal(map[string]cty.Value{ 240 "role_arn": cty.StringVal("valid-role-arn"), 241 "duration": cty.StringVal("30m"), 242 "external_id": cty.StringVal("valid-external-id"), 243 "policy": cty.StringVal("valid-policy"), 244 "session_name": cty.StringVal("valid-session-name"), 245 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), 246 }), 247 expectedDiags: nil, 248 }, 249 { 250 description: "Missing Role ARN", 251 input: cty.ObjectVal(map[string]cty.Value{ 252 "role_arn": cty.StringVal(""), 253 "duration": cty.StringVal("30m"), 254 "external_id": cty.StringVal("valid-external-id"), 255 "policy": cty.StringVal("valid-policy"), 256 "session_name": cty.StringVal("valid-session-name"), 257 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), 258 }), 259 expectedDiags: []string{ 260 "The attribute \"assume_role.role_arn\" is required by the backend.\n\nRefer to the backend documentation for additional information which attributes are required.", 261 }, 262 }, 263 { 264 description: "Invalid Duration", 265 input: cty.ObjectVal(map[string]cty.Value{ 266 "role_arn": cty.StringVal("valid-role-arn"), 267 "duration": cty.StringVal("invalid-duration"), 268 "external_id": cty.StringVal("valid-external-id"), 269 "policy": cty.StringVal("valid-policy"), 270 "session_name": cty.StringVal("valid-session-name"), 271 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), 272 }), 273 expectedDiags: []string{ 274 "The value \"invalid-duration\" cannot be parsed as a duration: time: invalid duration \"invalid-duration\"", 275 }, 276 }, 277 { 278 description: "Invalid Duration Length", 279 input: cty.ObjectVal(map[string]cty.Value{ 280 "role_arn": cty.StringVal("valid-role-arn"), 281 "duration": cty.StringVal("44h"), 282 "external_id": cty.StringVal("valid-external-id"), 283 "policy": cty.StringVal("valid-policy"), 284 "session_name": cty.StringVal("valid-session-name"), 285 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), 286 }), 287 expectedDiags: []string{ 288 "Duration must be between 15m0s and 12h0m0s, had 44h", 289 }, 290 }, 291 { 292 description: "Invalid External ID (Empty)", 293 input: cty.ObjectVal(map[string]cty.Value{ 294 "role_arn": cty.StringVal("valid-role-arn"), 295 "duration": cty.StringVal("30m"), 296 "external_id": cty.StringVal(""), 297 "policy": cty.StringVal("valid-policy"), 298 "session_name": cty.StringVal("valid-session-name"), 299 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), 300 }), 301 expectedDiags: []string{ 302 "The value cannot be empty or all whitespace", 303 }, 304 }, 305 { 306 description: "Invalid Policy (Empty)", 307 input: cty.ObjectVal(map[string]cty.Value{ 308 "role_arn": cty.StringVal("valid-role-arn"), 309 "duration": cty.StringVal("30m"), 310 "external_id": cty.StringVal("valid-external-id"), 311 "policy": cty.StringVal(""), 312 "session_name": cty.StringVal("valid-session-name"), 313 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), 314 }), 315 expectedDiags: []string{ 316 "The value cannot be empty or all whitespace", 317 }, 318 }, 319 { 320 description: "Invalid Session Name (Empty)", 321 input: cty.ObjectVal(map[string]cty.Value{ 322 "role_arn": cty.StringVal("valid-role-arn"), 323 "duration": cty.StringVal("30m"), 324 "external_id": cty.StringVal("valid-external-id"), 325 "policy": cty.StringVal("valid-policy"), 326 "session_name": cty.StringVal(""), 327 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:policy/valid-policy-arn")}), 328 }), 329 expectedDiags: []string{ 330 "The value cannot be empty or all whitespace", 331 }, 332 }, 333 { 334 description: "Invalid Policy ARN (Invalid ARN Format)", 335 input: cty.ObjectVal(map[string]cty.Value{ 336 "role_arn": cty.StringVal("valid-role-arn"), 337 "duration": cty.StringVal("30m"), 338 "external_id": cty.StringVal("valid-external-id"), 339 "policy": cty.StringVal("valid-policy"), 340 "session_name": cty.StringVal("valid-session-name"), 341 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("invalid-arn-format")}), 342 }), 343 expectedDiags: []string{ 344 "The value [\"invalid-arn-format\"] cannot be parsed as an ARN: arn: invalid prefix", 345 }, 346 }, 347 { 348 description: "Invalid Policy ARN (Not Starting with 'policy/')", 349 input: cty.ObjectVal(map[string]cty.Value{ 350 "role_arn": cty.StringVal("valid-role-arn"), 351 "duration": cty.StringVal("30m"), 352 "external_id": cty.StringVal("valid-external-id"), 353 "policy": cty.StringVal("valid-policy"), 354 "session_name": cty.StringVal("valid-session-name"), 355 "policy_arns": cty.ListVal([]cty.Value{cty.StringVal("arn:aws:iam::123456789012:role/invalid-policy-arn")}), 356 }), 357 expectedDiags: []string{ 358 "Value must be a valid IAM Policy ARN, got [\"arn:aws:iam::123456789012:role/invalid-policy-arn\"]", 359 }, 360 }, 361 } 362 363 for _, test := range tests { 364 t.Run(test.description, func(t *testing.T) { 365 diagnostics := validateNestedAssumeRole(test.input, cty.Path{cty.GetAttrStep{Name: "assume_role"}}) 366 if len(diagnostics) != len(test.expectedDiags) { 367 t.Errorf("Expected %d diagnostics, but got %d", len(test.expectedDiags), len(diagnostics)) 368 } 369 for i, diag := range diagnostics { 370 if diag.Description().Detail != test.expectedDiags[i] { 371 t.Errorf("Mismatch in diagnostic %d. Expected: %q, Got: %q", i, test.expectedDiags[i], diag.Description().Detail) 372 } 373 } 374 }) 375 } 376 }