pulumi/pkg/resource/stack/secrets_test.go
Pat Gavlin 40b0f8cbab
Cache ciphertext for secret properties. (#3183)
This caching is enabled by wrapping the `secrets.Manager` returned by
`DefaultSecretsProvider.OfType` in an outer `secrets.Manager` that
cooperates with `stack.{Serialize,Deserialize}PropertyValue`. Ciphertext
is cached on a per-secret-instance basis (i.e. not a per-plaintext-value
basis). Cached ciphertext is only reused if the plaintext for the secret
value has not changed. Entries are inserted into the cache upon both
encryption and decryption so that values that originated from ciphertext
and that have not changed can aoid re-encryption.

Contributes to #3178.
2019-09-18 15:52:31 -07:00

175 lines
5.5 KiB
Go

package stack
import (
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/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)
}
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)
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)
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)
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)
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)
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)
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)
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)
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)
assert.NoError(t, err)
assert.Equal(t, 3, sm.encryptCalls)
assert.Equal(t, barSer, barSer2)
}