ceph-csi/vendor/github.com/libopenstorage/secrets/vault/vault.go
Niels de Vos d7bcb42481 rebase: update libopenstorage/secrets
libopenstorage has added a new feature that makes it possible to destroy
the contents of a key/value in the Hashicorp Vault kv-v2 secrets backend.

See-also: https://github.com/libopenstorage/secrets/pull/55
Signed-off-by: Niels de Vos <ndevos@redhat.com>
2021-08-06 12:19:18 +00:00

415 lines
9.9 KiB
Go

package vault
import (
"fmt"
"path"
"strings"
"sync"
"github.com/hashicorp/vault/api"
"github.com/libopenstorage/secrets"
"github.com/libopenstorage/secrets/vault/utils"
)
const (
Name = secrets.TypeVault
DefaultBackendPath = "secret/"
VaultBackendPathKey = "VAULT_BACKEND_PATH"
VaultBackendKey = "VAULT_BACKEND"
kvVersionKey = "version"
kvDataKey = "data"
kvMetadataKey = "metadata"
kvVersion1 = "kv"
kvVersion2 = "kv-v2"
AuthMethodKubernetes = utils.AuthMethodKubernetes
AuthMethod = utils.AuthMethod
AuthMountPath = utils.AuthMountPath
AuthKubernetesRole = utils.AuthKubernetesRole
AuthKubernetesTokenPath = utils.AuthKubernetesTokenPath
AuthKubernetesMountPath = utils.AuthKubernetesMountPath
)
func init() {
if err := secrets.Register(Name, New); err != nil {
panic(err.Error())
}
}
type vaultSecrets struct {
mu sync.RWMutex
client *api.Client
currentNamespace string
lockClientToken sync.Mutex
endpoint string
backendPath string
namespace string
isKvBackendV2 bool
autoAuth bool
config map[string]interface{}
}
// These variables are helpful in testing to stub method call from packages
var (
newVaultClient = api.NewClient
isKvV2 = isKvBackendV2
)
func New(
secretConfig map[string]interface{},
) (secrets.Secrets, error) {
// DefaultConfig uses the environment variables if present.
config := api.DefaultConfig()
if len(secretConfig) == 0 && config.Error != nil {
return nil, config.Error
}
address := utils.GetVaultParam(secretConfig, api.EnvVaultAddress)
if address == "" {
return nil, utils.ErrVaultAddressNotSet
}
if err := utils.IsValidAddr(address); err != nil {
return nil, err
}
config.Address = address
if err := utils.ConfigureTLS(config, secretConfig); err != nil {
return nil, err
}
client, err := newVaultClient(config)
if err != nil {
return nil, err
}
namespace := utils.GetVaultParam(secretConfig, api.EnvVaultNamespace)
if len(namespace) > 0 {
// use a namespace as a header for setup purposes
// later use it as a key prefix
client.SetNamespace(namespace)
defer client.SetNamespace("")
}
token, autoAuth, err := utils.Authenticate(client, secretConfig)
if err != nil {
utils.CloseIdleConnections(config)
return nil, fmt.Errorf("failed to get the authentication token: %w", err)
}
client.SetToken(token)
backendPath := utils.GetVaultParam(secretConfig, VaultBackendPathKey)
if backendPath == "" {
backendPath = DefaultBackendPath
}
var isBackendV2 bool
backend := utils.GetVaultParam(secretConfig, VaultBackendKey)
if backend == kvVersion1 {
isBackendV2 = false
} else if backend == kvVersion2 {
isBackendV2 = true
} else {
// TODO: Handle backends other than kv
isBackendV2, err = isKvV2(client, backendPath)
if err != nil {
utils.CloseIdleConnections(config)
return nil, err
}
}
return &vaultSecrets{
endpoint: config.Address,
namespace: namespace,
currentNamespace: namespace,
client: client,
backendPath: backendPath,
isKvBackendV2: isBackendV2,
autoAuth: autoAuth,
config: secretConfig,
}, nil
}
func (v *vaultSecrets) String() string {
return Name
}
func (v *vaultSecrets) keyPath(secretID string, keyContext map[string]string) keyPath {
backendPath := v.backendPath
var namespace string
var ok bool
if namespace, ok = keyContext[secrets.KeyVaultNamespace]; !ok {
namespace = v.namespace
}
var isDestroyKey bool
if v.isKvBackendV2 {
if destroyAllSecrets, ok := keyContext[secrets.DestroySecret]; ok {
// checking for any value seems sufficient to assume 'destroy' is requested
if destroyAllSecrets != "" {
isDestroyKey = true
}
}
}
return keyPath{
backendPath: backendPath,
isBackendV2: v.isKvBackendV2,
namespace: namespace,
secretID: secretID,
isDestroyKey: isDestroyKey,
}
}
func (v *vaultSecrets) GetSecret(
secretID string,
keyContext map[string]string,
) (map[string]interface{}, error) {
key := v.keyPath(secretID, keyContext)
secretValue, err := v.read(key)
if err != nil {
return nil, fmt.Errorf("failed to get secret: %s: %s", key, err)
}
if secretValue == nil {
return nil, secrets.ErrInvalidSecretId
}
if v.isKvBackendV2 {
if data, exists := secretValue.Data[kvDataKey]; exists && data != nil {
if data, ok := data.(map[string]interface{}); ok {
return data, nil
}
}
return nil, secrets.ErrInvalidSecretId
} else {
return secretValue.Data, nil
}
}
func (v *vaultSecrets) PutSecret(
secretID string,
secretData map[string]interface{},
keyContext map[string]string,
) error {
if v.isKvBackendV2 {
secretData = map[string]interface{}{
kvDataKey: secretData,
}
}
key := v.keyPath(secretID, keyContext)
if _, err := v.write(key, secretData); err != nil {
return fmt.Errorf("failed to put secret: %s: %s", key, err)
}
return nil
}
func (v *vaultSecrets) DeleteSecret(
secretID string,
keyContext map[string]string,
) error {
key := v.keyPath(secretID, keyContext)
if _, err := v.delete(key); err != nil {
return fmt.Errorf("failed to delete secret: %s: %s", key, err)
}
return nil
}
func (v *vaultSecrets) Encrypt(
secretID string,
plaintTextData string,
keyContext map[string]string,
) (string, error) {
return "", secrets.ErrNotSupported
}
func (v *vaultSecrets) Decrypt(
secretID string,
encryptedData string,
keyContext map[string]string,
) (string, error) {
return "", secrets.ErrNotSupported
}
func (v *vaultSecrets) Rencrypt(
originalSecretID string,
newSecretID string,
originalKeyContext map[string]string,
newKeyContext map[string]string,
encryptedData string,
) (string, error) {
return "", secrets.ErrNotSupported
}
func (v *vaultSecrets) ListSecrets() ([]string, error) {
return nil, secrets.ErrNotSupported
}
func (v *vaultSecrets) read(path keyPath) (*api.Secret, error) {
if v.autoAuth {
v.lockClientToken.Lock()
defer v.lockClientToken.Unlock()
if err := v.setNamespaceToken(path.Namespace()); err != nil {
return nil, err
}
}
secretValue, err := v.lockedRead(path.Path())
if v.isTokenExpired(err) {
if err = v.renewToken(path.Namespace()); err != nil {
return nil, fmt.Errorf("failed to renew token: %s", err)
}
return v.lockedRead(path.Path())
}
return secretValue, err
}
func (v *vaultSecrets) write(path keyPath, data map[string]interface{}) (*api.Secret, error) {
if v.autoAuth {
v.lockClientToken.Lock()
defer v.lockClientToken.Unlock()
if err := v.setNamespaceToken(path.Namespace()); err != nil {
return nil, err
}
}
secretValue, err := v.lockedWrite(path.Path(), data)
if v.isTokenExpired(err) {
if err = v.renewToken(path.Namespace()); err != nil {
return nil, fmt.Errorf("failed to renew token: %s", err)
}
return v.lockedWrite(path.Path(), data)
}
return secretValue, err
}
func (v *vaultSecrets) delete(path keyPath) (*api.Secret, error) {
if v.autoAuth {
v.lockClientToken.Lock()
defer v.lockClientToken.Unlock()
if err := v.setNamespaceToken(path.Namespace()); err != nil {
return nil, err
}
}
secretValue, err := v.lockedDelete(path.Path())
if v.isTokenExpired(err) {
if err = v.renewToken(path.Namespace()); err != nil {
return nil, fmt.Errorf("failed to renew token: %s", err)
}
return v.lockedDelete(path.Path())
}
return secretValue, err
}
func (v *vaultSecrets) lockedRead(path string) (*api.Secret, error) {
v.mu.RLock()
defer v.mu.RUnlock()
return v.client.Logical().Read(path)
}
func (v *vaultSecrets) lockedWrite(path string, data map[string]interface{}) (*api.Secret, error) {
v.mu.RLock()
defer v.mu.RUnlock()
return v.client.Logical().Write(path, data)
}
func (v *vaultSecrets) lockedDelete(path string) (*api.Secret, error) {
v.mu.RLock()
defer v.mu.RUnlock()
return v.client.Logical().Delete(path)
}
func (v *vaultSecrets) renewToken(namespace string) error {
v.mu.Lock()
defer v.mu.Unlock()
if len(namespace) > 0 {
v.client.SetNamespace(namespace)
defer v.client.SetNamespace("")
}
token, err := utils.GetAuthToken(v.client, v.config)
if err != nil {
return fmt.Errorf("get auth token for %s namespace: %s", namespace, err)
}
v.currentNamespace = namespace
v.client.SetToken(token)
return nil
}
func (v *vaultSecrets) isTokenExpired(err error) bool {
return err != nil && v.autoAuth && strings.Contains(err.Error(), "permission denied")
}
// setNamespaceToken is used for a multi-token support with a kubernetes auto auth setup.
//
// This allows to talk with a multiple vault namespaces (which are not sub-namespace). Create
// the same “Kubernetes Auth Role” in each of the configured namespace. For every request it
// fetches the token for that specific namespace.
func (v *vaultSecrets) setNamespaceToken(namespace string) error {
if v.currentNamespace == namespace {
return nil
}
return v.renewToken(namespace)
}
func isKvBackendV2(client *api.Client, backendPath string) (bool, error) {
mounts, err := client.Sys().ListMounts()
if err != nil {
return false, err
}
for path, mount := range mounts {
// path is represented as 'path/'
if trimSlash(path) == trimSlash(backendPath) {
version := mount.Options[kvVersionKey]
if version == "2" {
return true, nil
}
return false, nil
}
}
return false, fmt.Errorf("Secrets engine with mount path '%s' not found",
backendPath)
}
func trimSlash(in string) string {
return strings.Trim(in, "/")
}
type keyPath struct {
backendPath string
isBackendV2 bool
namespace string
secretID string
isDestroyKey bool
}
func (k keyPath) Path() string {
if k.isBackendV2 {
keyType := kvDataKey
if k.isDestroyKey {
keyType = kvMetadataKey
}
return path.Join(k.namespace, k.backendPath, keyType, k.secretID)
}
return path.Join(k.namespace, k.backendPath, k.secretID)
}
func (k keyPath) Namespace() string {
return k.namespace
}
func (k keyPath) String() string {
return fmt.Sprintf("backendPath=%s, backendV2=%t, namespace=%s, secretID=%s", k.backendPath, k.isBackendV2, k.namespace, k.secretID)
}