package stack import ( "encoding/json" "fmt" "strings" "testing" "github.com/pkg/errors" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" "github.com/stretchr/testify/assert" ) type testSecretsManager struct { encryptCalls int decryptCalls int } func (t *testSecretsManager) Type() string { return "test" } func (t *testSecretsManager) State() interface{} { return nil } func (t *testSecretsManager) Encrypter() (config.Encrypter, error) { return t, nil } func (t *testSecretsManager) Decrypter() (config.Decrypter, error) { return t, nil } func (t *testSecretsManager) EncryptValue(plaintext string) (string, error) { t.encryptCalls++ return fmt.Sprintf("%v:%v", t.encryptCalls, plaintext), nil } func (t *testSecretsManager) DecryptValue(ciphertext string) (string, error) { t.decryptCalls++ i := strings.Index(ciphertext, ":") if i == -1 { return "", errors.New("invalid ciphertext format") } return ciphertext[i+1:], nil } func deserializeProperty(v interface{}, dec config.Decrypter) (resource.PropertyValue, error) { b, err := json.Marshal(v) if err != nil { return resource.PropertyValue{}, err } if err := json.Unmarshal(b, &v); err != nil { return resource.PropertyValue{}, err } return DeserializePropertyValue(v, dec, config.NewPanicCrypter()) } func TestCachingCrypter(t *testing.T) { sm := &testSecretsManager{} csm := NewCachingSecretsManager(sm) foo1 := resource.MakeSecret(resource.NewStringProperty("foo")) foo2 := resource.MakeSecret(resource.NewStringProperty("foo")) bar := resource.MakeSecret(resource.NewStringProperty("bar")) enc, err := csm.Encrypter() assert.NoError(t, err) // Serialize the first copy of "foo". Encrypt should be called once, as this value has not yet been encrypted. foo1Ser, err := SerializePropertyValue(foo1, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 1, sm.encryptCalls) // Serialize the second copy of "foo". Because this is a different secret instance, Encrypt should be called // a second time even though the plaintext is the same as the last value we encrypted. foo2Ser, err := SerializePropertyValue(foo2, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 2, sm.encryptCalls) assert.NotEqual(t, foo1Ser, foo2Ser) // Serialize "bar". Encrypt should be called once, as this value has not yet been encrypted. barSer, err := SerializePropertyValue(bar, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 3, sm.encryptCalls) // Serialize the first copy of "foo" again. Encrypt should not be called, as this value has already been // encrypted. foo1Ser2, err := SerializePropertyValue(foo1, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 3, sm.encryptCalls) assert.Equal(t, foo1Ser, foo1Ser2) // Serialize the second copy of "foo" again. Encrypt should not be called, as this value has already been // encrypted. foo2Ser2, err := SerializePropertyValue(foo2, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 3, sm.encryptCalls) assert.Equal(t, foo2Ser, foo2Ser2) // Serialize "bar" again. Encrypt should not be called, as this value has already been encrypted. barSer2, err := SerializePropertyValue(bar, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 3, sm.encryptCalls) assert.Equal(t, barSer, barSer2) dec, err := csm.Decrypter() assert.NoError(t, err) // Decrypt foo1Ser. Decrypt should be called. foo1Dec, err := deserializeProperty(foo1Ser, dec) assert.NoError(t, err) assert.True(t, foo1.DeepEquals(foo1Dec)) assert.Equal(t, 1, sm.decryptCalls) // Decrypt foo2Ser. Decrypt should be called. foo2Dec, err := deserializeProperty(foo2Ser, dec) assert.NoError(t, err) assert.True(t, foo2.DeepEquals(foo2Dec)) assert.Equal(t, 2, sm.decryptCalls) // Decrypt barSer. Decrypt should be called. barDec, err := deserializeProperty(barSer, dec) assert.NoError(t, err) assert.True(t, bar.DeepEquals(barDec)) assert.Equal(t, 3, sm.decryptCalls) // Create a new CachingSecretsManager and re-run the decrypts. Each decrypt should insert the plain- and // ciphertext into the cache with the associated secret. csm = NewCachingSecretsManager(sm) dec, err = csm.Decrypter() assert.NoError(t, err) // Decrypt foo1Ser. Decrypt should be called. foo1Dec, err = deserializeProperty(foo1Ser, dec) assert.NoError(t, err) assert.True(t, foo1.DeepEquals(foo1Dec)) assert.Equal(t, 4, sm.decryptCalls) // Decrypt foo2Ser. Decrypt should be called. foo2Dec, err = deserializeProperty(foo2Ser, dec) assert.NoError(t, err) assert.True(t, foo2.DeepEquals(foo2Dec)) assert.Equal(t, 5, sm.decryptCalls) // Decrypt barSer. Decrypt should be called. barDec, err = deserializeProperty(barSer, dec) assert.NoError(t, err) assert.True(t, bar.DeepEquals(barDec)) assert.Equal(t, 6, sm.decryptCalls) enc, err = csm.Encrypter() assert.NoError(t, err) // Serialize the first copy of "foo" again. Encrypt should not be called, as this value has already been // cached by the earlier calls to Decrypt. foo1Ser2, err = SerializePropertyValue(foo1Dec, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 3, sm.encryptCalls) assert.Equal(t, foo1Ser, foo1Ser2) // Serialize the second copy of "foo" again. Encrypt should not be called, as this value has already been // cached by the earlier calls to Decrypt. foo2Ser2, err = SerializePropertyValue(foo2Dec, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 3, sm.encryptCalls) assert.Equal(t, foo2Ser, foo2Ser2) // Serialize "bar" again. Encrypt should not be called, as this value has already been cached by the // earlier calls to Decrypt. barSer2, err = SerializePropertyValue(barDec, enc, false /* showSecrets */) assert.NoError(t, err) assert.Equal(t, 3, sm.encryptCalls) assert.Equal(t, barSer, barSer2) }