Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for Vault KV v2 backends #6115

Merged
merged 7 commits into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 28 additions & 1 deletion atc/creds/vault/api_client.go
Expand Up @@ -60,7 +60,34 @@ func NewAPIClient(logger lager.Logger, apiURL string, tlsConfig TLSConfig, authC
// Read must be called after a successful login has occurred or an
// un-authorized client will be used.
func (ac *APIClient) Read(path string) (*vaultapi.Secret, error) {
return ac.client().Logical().Read(path)
// Check if path is kv1 or kv2
path = sanitizePath(path)
mountPath, kv2, err := isKVv2(path, ac.client())
if err != nil {
return nil, err
}

// If the path is under a kv2 mount, add the /data/ path to the prefix
if kv2 {
path = addPrefixToVKVPath(path, mountPath, "data")
}

secret, err := ac.client().Logical().Read(path)
if err != nil || secret == nil {
return secret, err
}

// Need to discard the metadata object and pull the v2 data field up to match kv1
if kv2 {
if data, ok := secret.Data["data"]; ok && data != nil {
secret.Data = data.(map[string]interface{})
} else {
// Return a nil secret object if the secret was deleted, but not destroyed
return nil, nil
}
}

return secret, err
}

func (ac *APIClient) loginParams() map[string]interface{} {
Expand Down
91 changes: 91 additions & 0 deletions atc/creds/vault/vault_kvhelpers.go
@@ -0,0 +1,91 @@
package vault

import (
"errors"
"path"
"strings"

"github.com/hashicorp/vault/api"
)

// The below helper functions are taken from the github.com/hashicorp/vault repository
// as they are not exposed natively.
// https://github.com/hashicorp/vault/blob/4b790d2c42f406a980230c196eeeb5b9b52a7cf1/command/kv_helpers.go#L44-L116

func kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) {
r := client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path)
resp, err := client.RawRequest(r)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
// If we get a 404 we are using an older version of vault, default to
// version 1
if resp != nil && resp.StatusCode == 404 {
return "", 1, nil
}

return "", 0, err
}

secret, err := api.ParseSecret(resp.Body)
if err != nil {
return "", 0, err
}
if secret == nil {
return "", 0, errors.New("nil response from pre-flight request")
}
var mountPath string
if mountPathRaw, ok := secret.Data["path"]; ok {
mountPath = mountPathRaw.(string)
}
options := secret.Data["options"]
if options == nil {
return mountPath, 1, nil
}
versionRaw := options.(map[string]interface{})["version"]
if versionRaw == nil {
return mountPath, 1, nil
}
version := versionRaw.(string)
switch version {
case "", "1":
return mountPath, 1, nil
case "2":
return mountPath, 2, nil
}

return mountPath, 1, nil
}

func isKVv2(path string, client *api.Client) (string, bool, error) {
mountPath, version, err := kvPreflightVersionRequest(client, path)
if err != nil {
return "", false, err
}

return mountPath, version == 2, nil
}

func addPrefixToVKVPath(p, mountPath, apiPrefix string) string {
switch {
case p == mountPath, p == strings.TrimSuffix(mountPath, "/"):
return path.Join(mountPath, apiPrefix)
default:
p = strings.TrimPrefix(p, mountPath)
return path.Join(mountPath, apiPrefix, p)
}
}

// https://github.com/hashicorp/vault/blob/4b790d2c42f406a980230c196eeeb5b9b52a7cf1/vault/logical_system.go#L3624-L3634
func sanitizePath(path string) string {
if !strings.HasSuffix(path, "/") {
path += "/"
}

if strings.HasPrefix(path, "/") {
path = path[1:]
}

return path
}