github.com/opentofu/opentofu@v1.7.1/internal/encryption/keyprovider/compliancetest/compliance.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 compliancetest
     7  
     8  import (
     9  	"bytes"
    10  	"encoding/json"
    11  	"errors"
    12  	"reflect"
    13  	"testing"
    14  
    15  	"github.com/hashicorp/hcl/v2/gohcl"
    16  	"github.com/opentofu/opentofu/internal/encryption/compliancetest"
    17  	"github.com/opentofu/opentofu/internal/encryption/config"
    18  	"github.com/opentofu/opentofu/internal/encryption/keyprovider"
    19  )
    20  
    21  func ComplianceTest[TDescriptor keyprovider.Descriptor, TConfig keyprovider.Config, TMeta keyprovider.KeyMeta, TKeyProvider keyprovider.KeyProvider](
    22  	t *testing.T,
    23  	config TestConfiguration[TDescriptor, TConfig, TMeta, TKeyProvider],
    24  ) {
    25  	var cfg TConfig
    26  	cfgType := reflect.TypeOf(cfg)
    27  	if cfgType.Kind() != reflect.Ptr || cfgType.Elem().Kind() != reflect.Struct {
    28  		compliancetest.Fail(t, "You declared the config type to be %T, but it should be a pointer to a struct. Please fix your call to ComplianceTest().", cfg)
    29  	}
    30  
    31  	var meta TMeta
    32  	metaType := reflect.TypeOf(cfg)
    33  	if metaType.Kind() != reflect.Interface {
    34  		if metaType.Kind() != reflect.Ptr || metaType.Elem().Kind() != reflect.Struct {
    35  			compliancetest.Log(t, "You declared a metadata type as %T, but it should be a pointer to a struct. Please fix your call to ComplianceTest().", meta)
    36  		}
    37  	} else {
    38  		compliancetest.Log(t, "Metadata type declared as interface{}, assuming the key provider does not need metadata. (This will be validated later.)")
    39  	}
    40  
    41  	t.Run("ID", func(t *testing.T) {
    42  		complianceTestID(t, config)
    43  	})
    44  
    45  	t.Run("ConfigStruct", func(t *testing.T) {
    46  		compliancetest.ConfigStruct[TConfig](t, config.Descriptor.ConfigStruct())
    47  
    48  		t.Run("hcl-parsing", func(t *testing.T) {
    49  			if config.HCLParseTestCases == nil {
    50  				compliancetest.Fail(t, "Please provide a map in HCLParseTestCases.")
    51  			}
    52  			for name, tc := range config.HCLParseTestCases {
    53  				tc := tc
    54  				t.Run(name, func(t *testing.T) {
    55  					complianceTestHCLParsingTestCase(t, tc, config)
    56  				})
    57  			}
    58  		})
    59  
    60  		t.Run("config", func(t *testing.T) {
    61  			if config.ConfigStructTestCases == nil {
    62  				compliancetest.Fail(t, "Please provide a map in ConfigStructTestCases.")
    63  			}
    64  			for name, tc := range config.ConfigStructTestCases {
    65  				tc := tc
    66  				t.Run(name, func(t *testing.T) {
    67  					complianceTestConfigCase[TConfig, TKeyProvider, TMeta](t, tc)
    68  				})
    69  			}
    70  		})
    71  	})
    72  
    73  	t.Run("metadata", func(t *testing.T) {
    74  		if config.MetadataStructTestCases == nil {
    75  			compliancetest.Fail(t, "Please provide a map in MetadataStructTestCases.")
    76  		}
    77  		for name, tc := range config.MetadataStructTestCases {
    78  			tc := tc
    79  			t.Run(name, func(t *testing.T) {
    80  				complianceTestMetadataTestCase[TConfig, TKeyProvider, TMeta](t, tc)
    81  			})
    82  		}
    83  	})
    84  
    85  	t.Run("provide", func(t *testing.T) {
    86  		complianceTestProvide[TDescriptor, TConfig, TKeyProvider, TMeta](t, config)
    87  	})
    88  
    89  	t.Run("test-completeness", func(t *testing.T) {
    90  		t.Run("HCL", func(t *testing.T) {
    91  			hasNotValidHCL := false
    92  			hasValidHCLNotValidBuild := false
    93  			hasValidHCLAndBuild := false
    94  			for _, tc := range config.HCLParseTestCases {
    95  				if !tc.ValidHCL {
    96  					hasNotValidHCL = true
    97  				} else {
    98  					if tc.ValidBuild {
    99  						hasValidHCLAndBuild = true
   100  					} else {
   101  						hasValidHCLNotValidBuild = true
   102  					}
   103  				}
   104  			}
   105  			if !hasNotValidHCL {
   106  				compliancetest.Fail(t, "Please define at least one test with an invalid HCL.")
   107  			}
   108  			if !hasValidHCLNotValidBuild {
   109  				compliancetest.Fail(t, "Please define at least one test with a valid HCL that will fail on Build() for validation.")
   110  			}
   111  			if !hasValidHCLAndBuild {
   112  				compliancetest.Fail(t, "Please define at least one test with a valid HCL that will succeed on Build() for validation.")
   113  			}
   114  		})
   115  		t.Run("metadata", func(t *testing.T) {
   116  			hasNotPresent := false
   117  			hasNotValid := false
   118  			hasValid := false
   119  			for _, tc := range config.MetadataStructTestCases {
   120  				if !tc.IsPresent {
   121  					hasNotPresent = true
   122  				} else {
   123  					if tc.IsValid {
   124  						hasValid = true
   125  					} else {
   126  						hasNotValid = true
   127  					}
   128  				}
   129  			}
   130  			if !hasNotPresent {
   131  				compliancetest.Fail(t, "Please provide at least one metadata test that represents non-present metadata.")
   132  			}
   133  			if !hasNotValid {
   134  				compliancetest.Log(t, "Warning: Please provide at least one metadata test that represents an invalid metadata that is present.")
   135  			}
   136  			if !hasValid {
   137  				compliancetest.Log(t, "Warning: Please provide at least one metadata test that represents a valid metadata.")
   138  			}
   139  		})
   140  	})
   141  }
   142  
   143  func complianceTestProvide[TDescriptor keyprovider.Descriptor, TConfig keyprovider.Config, TKeyProvider keyprovider.KeyProvider, TMeta keyprovider.KeyMeta](
   144  	t *testing.T,
   145  	cfg TestConfiguration[TDescriptor, TConfig, TMeta, TKeyProvider],
   146  ) {
   147  	if reflect.ValueOf(cfg.ProvideTestCase.ValidConfig).IsNil() {
   148  		compliancetest.Fail(t, "Please provide a ValidConfig in ProvideTestCase.")
   149  	}
   150  	keyProviderConfig := cfg.ProvideTestCase.ValidConfig
   151  	t.Run("nil-metadata", func(t *testing.T) {
   152  		keyProvider, inMeta := complianceTestBuildConfigAndValidate[TKeyProvider, TMeta](t, keyProviderConfig, true)
   153  
   154  		if reflect.ValueOf(inMeta).IsNil() {
   155  			compliancetest.Skip(t, "The key provider does not have metadata (no metadata returned from Build()).")
   156  			return
   157  		}
   158  		_, _, err := keyProvider.Provide(nil)
   159  		if err == nil {
   160  			compliancetest.Fail(t, "Provide() did not return no error when provided with nil metadata.")
   161  		} else {
   162  			compliancetest.Log(t, "Provide() correctly returned an error when provided with nil metadata (%v).", err)
   163  		}
   164  		var typedError *keyprovider.ErrInvalidMetadata
   165  		if !errors.As(err, &typedError) {
   166  			compliancetest.Fail(t, "Provide() returned an error of the type %T instead of %T. Please use the correct typed errors.", err, typedError)
   167  		} else {
   168  			compliancetest.Log(t, "Provide() correctly returned a %T when provided with nil metadata.", typedError)
   169  		}
   170  	})
   171  	t.Run("incorrect-metadata-type", func(t *testing.T) {
   172  		keyProvider, inMeta := complianceTestBuildConfigAndValidate[TKeyProvider, TMeta](t, keyProviderConfig, true)
   173  		if reflect.ValueOf(inMeta).IsNil() {
   174  			compliancetest.Skip(t, "The key provider does not have metadata (no metadata returned from Build()).")
   175  			return
   176  		}
   177  		_, _, err := keyProvider.Provide(&struct{}{})
   178  		if err == nil {
   179  			compliancetest.Fail(t, "Provide() did not return no error when provided with an incorrect metadata type.")
   180  		} else {
   181  			compliancetest.Log(t, "Provide() correctly returned an error when provided with an metadata type (%v).", err)
   182  		}
   183  		var typedError *keyprovider.ErrInvalidMetadata
   184  		if !errors.As(err, &typedError) {
   185  			compliancetest.Fail(t, "Provide() returned an error of the type %T instead of %T. Please use the correct typed errors.", err, typedError)
   186  		} else {
   187  			compliancetest.Log(t, "Provide() correctly returned a %T when provided with an incorrect metadata type.", typedError)
   188  		}
   189  	})
   190  	t.Run("round-trip", func(t *testing.T) {
   191  		complianceTestRoundTrip(t, keyProviderConfig, cfg)
   192  	})
   193  }
   194  
   195  func complianceTestRoundTrip[TDescriptor keyprovider.Descriptor, TConfig keyprovider.Config, TKeyProvider keyprovider.KeyProvider, TMeta keyprovider.KeyMeta](
   196  	t *testing.T,
   197  	keyProviderConfig TConfig,
   198  	cfg TestConfiguration[TDescriptor, TConfig, TMeta, TKeyProvider],
   199  ) {
   200  	keyProvider, inMeta := complianceTestBuildConfigAndValidate[TKeyProvider, TMeta](t, keyProviderConfig, true)
   201  	output, outMeta, err := keyProvider.Provide(inMeta)
   202  	if err != nil {
   203  		compliancetest.Fail(t, "Provide() failed (%v).", err)
   204  	} else {
   205  		compliancetest.Log(t, "Provide() succeeded.")
   206  	}
   207  	if cfg.ProvideTestCase.ValidateMetadata != nil {
   208  		if err := cfg.ProvideTestCase.ValidateMetadata(outMeta.(TMeta)); err != nil {
   209  			compliancetest.Fail(t, "The metadata after the second Provide() call failed the test (%v).", err)
   210  		}
   211  	}
   212  
   213  	// Create a second key provider to avoid internal state.
   214  	keyProvider2, inMeta2 := complianceTestBuildConfigAndValidate[TKeyProvider, TMeta](t, keyProviderConfig, true)
   215  
   216  	marshalledMeta, err := json.Marshal(outMeta)
   217  	if err != nil {
   218  		compliancetest.Fail(t, "JSON-marshalling output meta failed (%v).", err)
   219  	} else {
   220  		compliancetest.Log(t, "JSON-marshalling output meta succeeded: %s", marshalledMeta)
   221  	}
   222  
   223  	if err := json.Unmarshal(marshalledMeta, &inMeta2); err != nil {
   224  		compliancetest.Fail(t, "JSON-unmarshalling meta failed (%v).", err)
   225  	} else {
   226  		compliancetest.Log(t, "JSON-unmarshalling meta succeeded.")
   227  	}
   228  
   229  	output2, outMeta2, err := keyProvider2.Provide(inMeta2)
   230  	if err != nil {
   231  		compliancetest.Fail(t, "Provide() on the subsequent run failed (%v).", err)
   232  	} else {
   233  		compliancetest.Log(t, "Provide() on the subsequent run succeeded.")
   234  	}
   235  
   236  	if cfg.ProvideTestCase.ExpectedOutput != nil {
   237  		if !bytes.Equal(cfg.ProvideTestCase.ExpectedOutput.EncryptionKey, output.EncryptionKey) {
   238  			compliancetest.Fail(t, "Incorrect encryption key received after the first Provide() call. Please set a break point to the line of this error message to debug this error.")
   239  		}
   240  		if !bytes.Equal(cfg.ProvideTestCase.ExpectedOutput.DecryptionKey, output2.DecryptionKey) {
   241  			compliancetest.Fail(t, "Incorrect decryption key received after the second Provide() call. Please set a break point to the line of this error message to debug this error.")
   242  		}
   243  		if !bytes.Equal(cfg.ProvideTestCase.ExpectedOutput.EncryptionKey, output2.EncryptionKey) {
   244  			compliancetest.Fail(t, "Incorrect encryption key received after the second Provide() call. Please set a break point to the line of this error message to debug this error.")
   245  		}
   246  	}
   247  	if cfg.ProvideTestCase.ValidateMetadata != nil {
   248  		if err := cfg.ProvideTestCase.ValidateMetadata(outMeta2.(TMeta)); err != nil {
   249  			compliancetest.Fail(t, "The metadata after the second Provide() call failed the test (%v).", err)
   250  		}
   251  	}
   252  	if cfg.ProvideTestCase.ValidateKeys == nil {
   253  		if !bytes.Equal(output2.DecryptionKey, output.EncryptionKey) {
   254  			compliancetest.Fail(
   255  				t,
   256  				"The encryption key from the first call to Provide() does not match the decryption key provided by the second Provide() call. If you intend the two keys to be different, please provide an ProvideTestCase.ValidateKeys function. If this is not intended, please set a break point to the line of this error message.",
   257  			)
   258  		} else {
   259  			compliancetest.Log(
   260  				t,
   261  				"The encryption and decryption keys match.",
   262  			)
   263  		}
   264  	} else {
   265  		if err := cfg.ProvideTestCase.ValidateKeys(output2.DecryptionKey, output.EncryptionKey); err != nil {
   266  			compliancetest.Fail(
   267  				t,
   268  				"The encryption key from the first call to Provide() does not match the decryption key provided by the second Provide() call (%v),",
   269  				err,
   270  			)
   271  		} else {
   272  			compliancetest.Log(
   273  				t,
   274  				"The encryption and decryption keys match.",
   275  			)
   276  		}
   277  	}
   278  }
   279  
   280  func complianceTestID[TDescriptor keyprovider.Descriptor, TConfig keyprovider.Config, TMeta keyprovider.KeyMeta, TKeyProvider keyprovider.KeyProvider](
   281  	t *testing.T,
   282  	config TestConfiguration[TDescriptor, TConfig, TMeta, TKeyProvider],
   283  ) {
   284  	id := config.Descriptor.ID()
   285  	if id == "" {
   286  		compliancetest.Fail(t, "ID is empty.")
   287  	} else {
   288  		compliancetest.Log(t, "ID is not empty.")
   289  	}
   290  	if err := id.Validate(); err != nil {
   291  		compliancetest.Fail(t, "ID failed validation: %s", id)
   292  	} else {
   293  		compliancetest.Log(t, "ID passed validation.")
   294  	}
   295  }
   296  
   297  func complianceTestHCLParsingTestCase[TDescriptor keyprovider.Descriptor, TConfig keyprovider.Config, TMeta keyprovider.KeyMeta, TKeyProvider keyprovider.KeyProvider](
   298  	t *testing.T,
   299  	tc HCLParseTestCase[TConfig, TKeyProvider],
   300  	cfg TestConfiguration[TDescriptor, TConfig, TMeta, TKeyProvider],
   301  ) {
   302  	parseError := false
   303  	parsedConfig, diags := config.LoadConfigFromString("config.hcl", tc.HCL)
   304  	if tc.ValidHCL {
   305  		if diags.HasErrors() {
   306  			compliancetest.Fail(t, "Unexpected HCL error (%v).", diags)
   307  		} else {
   308  			compliancetest.Log(t, "HCL successfully parsed.")
   309  		}
   310  	} else {
   311  		if diags.HasErrors() {
   312  			parseError = true
   313  		}
   314  	}
   315  
   316  	configStruct := cfg.Descriptor.ConfigStruct()
   317  	diags = gohcl.DecodeBody(
   318  		parsedConfig.KeyProviderConfigs[0].Body,
   319  		nil,
   320  		configStruct,
   321  	)
   322  	var keyProvider TKeyProvider
   323  	if tc.ValidHCL {
   324  		if diags.HasErrors() {
   325  			compliancetest.Fail(t, "Failed to parse empty HCL block into config struct (%v).", diags)
   326  		} else {
   327  			compliancetest.Log(t, "HCL successfully loaded into config struct.")
   328  		}
   329  
   330  		keyProvider, _ = complianceTestBuildConfigAndValidate[TKeyProvider, TMeta](t, configStruct, tc.ValidBuild)
   331  	} else {
   332  		if !parseError && !diags.HasErrors() {
   333  			compliancetest.Fail(t, "Expected error during HCL parsing, but no error was returned.")
   334  		} else {
   335  			compliancetest.Log(t, "HCL loading errored correctly (%v).", diags)
   336  		}
   337  	}
   338  
   339  	if tc.Validate != nil {
   340  		if err := tc.Validate(configStruct.(TConfig), keyProvider); err != nil {
   341  			compliancetest.Fail(t, "Error during validation and configuration (%v).", err)
   342  		} else {
   343  			compliancetest.Log(t, "Successfully validated parsed HCL config and applied modifications.")
   344  		}
   345  	} else {
   346  		compliancetest.Log(t, "No ValidateAndConfigure provided, skipping HCL parse validation.")
   347  	}
   348  }
   349  
   350  func complianceTestConfigCase[TConfig keyprovider.Config, TKeyProvider keyprovider.KeyProvider, TMeta keyprovider.KeyMeta](
   351  	t *testing.T,
   352  	tc ConfigStructTestCase[TConfig, TKeyProvider],
   353  ) {
   354  	keyProvider, _ := complianceTestBuildConfigAndValidate[TKeyProvider, TMeta](t, tc.Config, tc.ValidBuild)
   355  	if tc.Validate != nil {
   356  		if err := tc.Validate(keyProvider); err != nil {
   357  			compliancetest.Fail(t, "Error during validation and configuration (%v).", err)
   358  		} else {
   359  			compliancetest.Log(t, "Successfully validated parsed HCL config and applied modifications.")
   360  		}
   361  	} else {
   362  		compliancetest.Log(t, "No ValidateAndConfigure provided, skipping HCL parse validation.")
   363  	}
   364  }
   365  
   366  func complianceTestBuildConfigAndValidate[TKeyProvider keyprovider.KeyProvider, TMeta keyprovider.KeyMeta](
   367  	t *testing.T,
   368  	configStruct keyprovider.Config,
   369  	validBuild bool,
   370  ) (TKeyProvider, TMeta) {
   371  	if configStruct == nil {
   372  		compliancetest.Fail(t, "Nil struct passed!")
   373  	}
   374  
   375  	var typedKeyProvider TKeyProvider
   376  	var typedMeta TMeta
   377  	var ok bool
   378  	kp, meta, err := configStruct.Build()
   379  	if validBuild {
   380  		if err != nil {
   381  			compliancetest.Fail(t, "Build() returned an unexpected error: %v.", err)
   382  		} else {
   383  			compliancetest.Log(t, "Build() did not return an error.")
   384  		}
   385  		typedKeyProvider, ok = kp.(TKeyProvider)
   386  		if !ok {
   387  			compliancetest.Fail(t, "Build() returned an invalid key provider type of %T, expected %T", kp, typedKeyProvider)
   388  		} else {
   389  			compliancetest.Log(t, "Build() returned the correct key provider type of %T.", typedKeyProvider)
   390  		}
   391  
   392  		metaType := reflect.TypeOf(typedMeta)
   393  		if meta == nil {
   394  			if metaType.Kind() != reflect.Interface {
   395  				compliancetest.Fail(t, "Build() did not return a metadata, but you declared a metadata type. Please make sure that you always return the same metadata type.")
   396  			} else {
   397  				compliancetest.Log(t, "Build() did not return a metadata and the declared metadata type is interface{}.")
   398  			}
   399  		} else {
   400  			if metaType.Kind() == reflect.Interface {
   401  				compliancetest.Fail(t, "Build() returned metadata, but you declared an interface type as the metadata type. Please always declare a pointer to a struct as a metadata type.")
   402  			} else {
   403  				compliancetest.Log(t, "Build() returned metadata and the declared metadata type is not an interface.")
   404  			}
   405  			typedMeta, ok = meta.(TMeta)
   406  			if !ok {
   407  				compliancetest.Fail(t, "Build() returned an invalid metadata type of %T, expected %T", meta, typedMeta)
   408  			} else {
   409  				compliancetest.Log(t, "Build() returned the correct metadata type of %T.", meta)
   410  			}
   411  		}
   412  	} else {
   413  		if err == nil {
   414  			compliancetest.Fail(t, "Build() did not return an error.")
   415  		} else {
   416  			compliancetest.Log(t, "Build() correctly returned an error: %v", err)
   417  		}
   418  
   419  		var typedError *keyprovider.ErrInvalidConfiguration
   420  		if !errors.As(err, &typedError) {
   421  			compliancetest.Fail(
   422  				t,
   423  				"Build() did not return the correct error type, got %T but expected %T",
   424  				err,
   425  				typedError,
   426  			)
   427  		} else {
   428  			compliancetest.Log(t, "Build() returned the correct error type of %T", typedError)
   429  		}
   430  	}
   431  	return typedKeyProvider, typedMeta
   432  }
   433  
   434  func complianceTestMetadataTestCase[TConfig keyprovider.Config, TKeyProvider keyprovider.KeyProvider, TMeta keyprovider.KeyMeta](
   435  	t *testing.T,
   436  	tc MetadataStructTestCase[TConfig, TMeta],
   437  ) {
   438  	keyProvider, _ := complianceTestBuildConfigAndValidate[TKeyProvider, TMeta](t, tc.ValidConfig, true)
   439  
   440  	output, _, err := keyProvider.Provide(tc.Meta)
   441  	if tc.IsPresent {
   442  		// This test case means that the input metadata should be considered present, so it's either an error or a
   443  		// decryption key.
   444  		if tc.IsValid {
   445  			if err != nil {
   446  				var typedError *keyprovider.ErrKeyProviderFailure
   447  				if !errors.As(err, &typedError) {
   448  					compliancetest.Fail(
   449  						t,
   450  						"The Provide() function returned an unexpected error, which was also of the incorrect type of %T instead of %T: %v",
   451  						err,
   452  						typedError,
   453  						err,
   454  					)
   455  				}
   456  				compliancetest.Fail(t, "The Provide() function returned an unexpected error: %v", err)
   457  			}
   458  		} else {
   459  			if err == nil {
   460  				compliancetest.Fail(t, "The Provide() function did not return an error as expected.")
   461  			} else {
   462  				compliancetest.Log(t, "The Provide() function returned an expected error: %v", err)
   463  			}
   464  
   465  			var typedError *keyprovider.ErrInvalidMetadata
   466  			if !errors.As(err, &typedError) {
   467  				compliancetest.Fail(
   468  					t,
   469  					"The Provide() function returned the error type of %T instead of %T. Please use the correct typed errors.",
   470  					err,
   471  					typedError,
   472  				)
   473  			}
   474  		}
   475  	} else {
   476  		if err != nil {
   477  			var typedError *keyprovider.ErrKeyProviderFailure
   478  			if !errors.As(err, &typedError) {
   479  				compliancetest.Fail(
   480  					t,
   481  					"The Provide() function returned an unexpected error, which was also of the incorrect type of %T instead of %T: %v",
   482  					err,
   483  					typedError,
   484  					err,
   485  				)
   486  			}
   487  			compliancetest.Fail(t, "The Provide() function returned an unexpected error: %v", err)
   488  		}
   489  		if len(output.DecryptionKey) != 0 {
   490  			compliancetest.Fail(
   491  				t,
   492  				"The Provide() function a decryption key despite not receiving input meta. This is incorrect, please don't return a decryption key unless you receive the input metadata.",
   493  			)
   494  		} else {
   495  			compliancetest.Log(
   496  				t,
   497  				"The Provide() function correctly did not return a decryption key without input metadata.",
   498  			)
   499  		}
   500  	}
   501  }