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.
174 lines
5.5 KiB
Go
174 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)
|
|
}
|