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  }