1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/pkg/vault/client_test.go
Siarhei Pazdniakou 0673d3fed6
Writing secrets to vault (#2770)
Co-authored-by: Kevin Stiehl <kevin.stiehl@numericas.de>
Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
2021-05-06 10:27:23 +02:00

488 lines
15 KiB
Go

package vault
import (
"encoding/json"
"fmt"
"path"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/mock"
mocks "github.com/SAP/jenkins-library/pkg/vault/mocks"
"github.com/hashicorp/vault/api"
"github.com/stretchr/testify/assert"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
)
type SecretData = map[string]interface{}
const (
sysLookupPath = "sys/internal/ui/mounts/"
)
func TestGetKV2Secret(t *testing.T) {
t.Run("Test missing secret", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
setupMockKvV2(vaultMock)
vaultMock.On("Read", "secret/data/notexist").Return(nil, nil)
secret, err := client.GetKvSecret("secret/notexist")
assert.NoError(t, err, "Missing secret should not an error")
assert.Nil(t, secret)
})
t.Run("Test parsing KV2 secrets", func(t *testing.T) {
t.Parallel()
const secretAPIPath = "secret/data/test"
const secretName = "secret/test"
t.Run("Getting secret from KV engine (v2)", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
setupMockKvV2(vaultMock)
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", secretAPIPath).Return(kv2Secret(SecretData{"key1": "value1"}), nil)
secret, err := client.GetKvSecret(secretName)
assert.NoError(t, err, "Expect GetKvSecret to succeed")
assert.Equal(t, "value1", secret["key1"])
})
t.Run("field ignored when 'data' field can't be parsed", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
setupMockKvV2(vaultMock)
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", secretAPIPath).Return(kv2Secret(SecretData{"key1": "value1", "key2": 5}), nil)
secret, err := client.GetKvSecret(secretName)
assert.NoError(t, err)
assert.Empty(t, secret["key2"])
})
t.Run("error is thrown when data field is missing", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
setupMockKvV2(vaultMock)
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", secretAPIPath).Return(kv1Secret(SecretData{"key1": "value1"}), nil)
secret, err := client.GetKvSecret(secretName)
assert.Error(t, err, "Expected to fail since 'data' field is missing")
assert.Nil(t, secret)
})
})
}
func TestGetKV1Secret(t *testing.T) {
t.Parallel()
const secretName = "secret/test"
t.Run("Test missing secret", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
setupMockKvV1(vaultMock)
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", mock.AnythingOfType("string")).Return(nil, nil)
secret, err := client.GetKvSecret("secret/notexist")
assert.NoError(t, err, "Missing secret should not an error")
assert.Nil(t, secret)
})
t.Run("Test parsing KV1 secrets", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
setupMockKvV1(vaultMock)
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", secretName).Return(kv1Secret(SecretData{"key1": "value1"}), nil)
secret, err := client.GetKvSecret(secretName)
assert.NoError(t, err)
assert.Equal(t, "value1", secret["key1"])
})
t.Run("Test parsing KV1 secrets", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
setupMockKvV1(vaultMock)
vaultMock.On("Read", secretName).Return(kv1Secret(SecretData{"key1": 5}), nil)
client := Client{vaultMock, &Config{}}
secret, err := client.GetKvSecret(secretName)
assert.NoError(t, err)
assert.Empty(t, secret["key1"])
})
}
func TestWriteKvSecret(t *testing.T) {
const secretName = "secret/test"
tests := []struct {
name string
initialSecret map[string]string
writingSecret map[string]string
expectedSecret map[string]string
}{
{
name: "Test write new KV2 secret",
initialSecret: nil,
writingSecret: map[string]string{"key": "value"},
expectedSecret: map[string]string{"key": "value"},
},
{
name: "Test rewrite KV2 secret with new keys",
initialSecret: map[string]string{"key1": "value1"},
writingSecret: map[string]string{"key2": "value2"},
expectedSecret: map[string]string{"key1": "value1", "key2": "value2"},
},
{
name: "Test rewrite KV2 secret with existed keys",
initialSecret: map[string]string{"key1": "value1"},
writingSecret: map[string]string{"key1": "value2"},
expectedSecret: map[string]string{"key1": "value2"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cluster := vault.NewTestCluster(t, &vault.CoreConfig{
DevToken: "token",
}, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
core := cluster.Cores[0].Core
vault.TestWaitActive(t, core)
vaultClient := cluster.Cores[0].Client
client := Client{vaultClient.Logical(), &Config{}}
cluster.Start()
defer cluster.Cleanup()
if test.initialSecret != nil {
err := client.WriteKvSecret(secretName, test.initialSecret)
assert.NoError(t, err)
}
err := client.WriteKvSecret(secretName, test.writingSecret)
assert.NoError(t, err)
secret, err := client.GetKvSecret(secretName)
assert.NoError(t, err)
assert.Equal(t, test.expectedSecret, secret)
})
}
}
func TestSecretIDGeneration(t *testing.T) {
t.Parallel()
const secretID = "secret-id"
const appRoleName = "test"
const appRolePath = "auth/approle/role/test"
t.Run("Test generating new secret-id", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
now := time.Now()
expiry := now.Add(5 * time.Hour).Format(time.RFC3339)
metadata := map[string]interface{}{
"field1": "value1",
}
metadataJSON, err := json.Marshal(metadata)
assert.NoError(t, err)
vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{
"expiration_time": expiry,
"metadata": metadata,
}), nil)
vaultMock.On("Write", path.Join(appRolePath, "/secret-id"), mapWith("metadata", string(metadataJSON))).Return(kv1Secret(SecretData{
"secret_id": "newSecretId",
}), nil)
newSecretID, err := client.GenerateNewAppRoleSecret(secretID, appRoleName)
assert.NoError(t, err)
assert.Equal(t, "newSecretId", newSecretID)
})
t.Run("Test with no secret-id returned", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
now := time.Now()
expiry := now.Add(5 * time.Hour).Format(time.RFC3339)
metadata := map[string]interface{}{
"field1": "value1",
}
metadataJSON, err := json.Marshal(metadata)
assert.NoError(t, err)
vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{
"expiration_time": expiry,
"metadata": metadata,
}), nil)
vaultMock.On("Write", path.Join(appRolePath, "/secret-id"), mapWith("metadata", string(metadataJSON))).Return(kv1Secret(SecretData{}), nil)
newSecretID, err := client.GenerateNewAppRoleSecret(secretID, appRoleName)
assert.EqualError(t, err, fmt.Sprintf("Vault response for path %s did not contain a new secret-id", path.Join(appRolePath, "secret-id")))
assert.Equal(t, newSecretID, "")
})
t.Run("Test with no new secret-id returned", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
now := time.Now()
expiry := now.Add(5 * time.Hour).Format(time.RFC3339)
metadata := map[string]interface{}{
"field1": "value1",
}
metadataJSON, err := json.Marshal(metadata)
assert.NoError(t, err)
vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{
"expiration_time": expiry,
"metadata": metadata,
}), nil)
vaultMock.On("Write", path.Join(appRolePath, "/secret-id"), mapWith("metadata", string(metadataJSON))).Return(kv1Secret(nil), nil)
newSecretID, err := client.GenerateNewAppRoleSecret(secretID, appRoleName)
assert.EqualError(t, err, fmt.Sprintf("Could not generate new approle secret-id for approle path %s", path.Join(appRolePath, "secret-id")))
assert.Equal(t, newSecretID, "")
})
}
func TestSecretIDTtl(t *testing.T) {
t.Parallel()
const secretID = "secret-id"
const appRolePath = "auth/approle/role/test"
const appRoleName = "test"
t.Run("Test fetching secreID TTL", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
now := time.Now()
expiry := now.Add(5 * time.Hour).Format(time.RFC3339)
vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{
"expiration_time": expiry,
}), nil)
ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName)
assert.NoError(t, err)
assert.Equal(t, 5*time.Hour, ttl.Round(time.Hour))
})
t.Run("Test with no expiration time", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{}), nil)
ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName)
assert.EqualError(t, err, fmt.Sprintf("Could not load secret-id information from path %s", appRolePath))
assert.Equal(t, time.Duration(0), ttl)
})
t.Run("Test with wrong date format", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{
"expiration_time": time.Now().String(),
}), nil)
ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName)
assert.True(t, strings.HasPrefix(err.Error(), "parsing time"))
assert.Equal(t, time.Duration(0), ttl)
})
t.Run("Test with expired secret-id", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
now := time.Now()
expiry := now.Add(-5 * time.Hour).Format(time.RFC3339)
vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{
"expiration_time": expiry,
}), nil)
ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName)
assert.NoError(t, err)
assert.Equal(t, time.Duration(0), ttl)
})
}
func TestGetAppRoleName(t *testing.T) {
t.Parallel()
const secretID = "secret-id"
t.Run("Test that correct role name is returned", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(SecretData{
"meta": SecretData{
"role_name": "test",
},
}), nil)
appRoleName, err := client.GetAppRoleName()
assert.NoError(t, err)
assert.Equal(t, "test", appRoleName)
})
t.Run("Test without secret data", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(nil), nil)
appRoleName, err := client.GetAppRoleName()
assert.EqualError(t, err, "Could not lookup token information: auth/token/lookup-self")
assert.Empty(t, appRoleName)
})
t.Run("Test without metadata data", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(SecretData{}), nil)
appRoleName, err := client.GetAppRoleName()
assert.EqualError(t, err, "Token info did not contain metadata auth/token/lookup-self")
assert.Empty(t, appRoleName)
})
t.Run("Test without role name in metadata", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(SecretData{
"meta": SecretData{},
}), nil)
appRoleName, err := client.GetAppRoleName()
assert.Empty(t, appRoleName)
assert.NoError(t, err)
})
t.Run("Test that different role_name types are ignored", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(SecretData{
"meta": SecretData{
"role_name": 5,
},
}), nil)
appRoleName, err := client.GetAppRoleName()
assert.Empty(t, appRoleName)
assert.NoError(t, err)
})
}
func TestTokenRevocation(t *testing.T) {
t.Parallel()
t.Run("Test that revocation error is returned", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Write",
"auth/token/revoke-self",
mock.IsType(map[string]interface{}{})).Return(nil, errors.New("Test"))
err := client.RevokeToken()
assert.Errorf(t, err, "Test")
})
t.Run("Test that revocation endpoint is called", func(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Write",
"auth/token/revoke-self",
mock.IsType(map[string]interface{}{})).Return(nil, nil)
err := client.RevokeToken()
assert.NoError(t, err)
})
}
func TestUnknownKvVersion(t *testing.T) {
vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", "sys/internal/ui/mounts/secret/secret").Return(&api.Secret{
Data: map[string]interface{}{
"path": "secret",
"options": map[string]interface{}{
"version": "3",
},
}}, nil)
secret, err := client.GetKvSecret("/secret/secret")
assert.EqualError(t, err, "KV Engine in version 3 is currently not supported")
assert.Nil(t, secret)
}
func TestSetAppRoleMountPont(t *testing.T) {
client := Client{nil, &Config{}}
const newMountpoint = "auth/test"
client.SetAppRoleMountPoint("auth/test")
assert.Equal(t, newMountpoint, client.config.AppRoleMountPoint)
}
func setupMockKvV2(vaultMock *mocks.VaultMock) {
vaultMock.On("Read", mock.MatchedBy(func(path string) bool {
return strings.HasPrefix(path, sysLookupPath)
})).Return(func(path string) *api.Secret {
pathComponents := strings.Split(strings.TrimPrefix(path, "sys/internal/ui/mounts/"), "/")
mountpath := "/"
if len(pathComponents) > 1 {
mountpath = pathComponents[0]
}
return &api.Secret{
Data: map[string]interface{}{
"path": mountpath,
"options": map[string]interface{}{
"version": "2",
},
},
}
}, nil)
}
func setupMockKvV1(vaultMock *mocks.VaultMock) {
vaultMock.On("Read", mock.MatchedBy(func(path string) bool {
return strings.HasPrefix(path, sysLookupPath)
})).Return(func(path string) *api.Secret {
pathComponents := strings.Split(strings.TrimPrefix(path, "sys/internal/ui/mounts/"), "/")
mountpath := "/"
if len(pathComponents) > 1 {
mountpath = pathComponents[0]
}
return &api.Secret{
Data: map[string]interface{}{
"path": mountpath,
},
}
}, nil)
}
func kv1Secret(data SecretData) *api.Secret {
return &api.Secret{
Data: data,
}
}
func kv2Secret(data SecretData) *api.Secret {
return &api.Secret{
Data: SecretData{"data": data},
}
}
func mapWith(key, expectedValue string) interface{} {
return mock.MatchedBy(func(arg map[string]interface{}) bool {
valRaw, ok := arg[key]
if !ok {
return false
}
val, ok := valRaw.(string)
if !ok {
return false
}
return val == expectedValue
})
}