Skip to content

Commit

Permalink
Merge PR drone#8: Handle v2 storage engine paths properly
Browse files Browse the repository at this point in the history
  • Loading branch information
lkubb committed Feb 14, 2023
2 parents 07c87c5 + 2000b2c commit 17add00
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 6 deletions.
117 changes: 111 additions & 6 deletions plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ package plugin

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"path"
"strconv"
"strings"

"github.com/drone/drone-go/drone"
"github.com/drone/drone-go/plugin/secret"
"github.com/sirupsen/logrus"

"github.com/hashicorp/vault/api"
)
Expand Down Expand Up @@ -78,6 +85,8 @@ func (p *plugin) Find(ctx context.Context, req *secret.Request) (*drone.Secret,

// helper function returns the secret from vault.
func (p *plugin) find(path string) (map[string]string, error) {
isV2, path := p.rewritePath(path)

secret, err := p.client.Logical().Read(path)
if err != nil {
return nil, err
Expand All @@ -86,12 +95,18 @@ func (p *plugin) find(path string) (map[string]string, error) {
return nil, errors.New("secret not found")
}

// HACK: the vault v2 key value store is confusing
// and I could not quite figure out how to work with
// the api. This is the workaround I came up with.
v := secret.Data["data"]
if data, ok := v.(map[string]interface{}); ok {
secret.Data = data
// the V2 api includes both "data" and "metadata" fields within the top level "data" -- we only care about data.
// https://www.vaultproject.io/api-docs/secret/kv/kv-v1#sample-response
// v1 data schema:
// { properties: { data: { type: object, description: "the actual data" }}}
// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1
// v2 data schema:
// { properties: { data: { properties: { data: { type: object, description: "the actual data" }}}}}
if isV2 {
v := secret.Data["data"]
if data, ok := v.(map[string]interface{}); ok {
secret.Data = data
}
}

params := map[string]string{}
Expand All @@ -104,3 +119,93 @@ func (p *plugin) find(path string) (map[string]string, error) {
}
return params, err
}

// rewritePath rewrites a secret path if need be according to storage engine constraints; if it fails, it returns the
// original path.
//
// TL;DR: vault requires rewriting secret paths for the V2 engine mount points. This is most visible when you use the
// CLI to output curl strings:
// $ vault kv get \
// -output-curl-string \
// foo/versioned/bar
// curl -H "X-Vault-Request: true" \
// -H "X-Vault-Token: $(vault print token)" \
// https://vault.example.com/v1/foo/versioned/data/bar
//
// Note the addition of "data" in the output curl string. This only occurs for the v2 engine. This function
// reproduces the logic from the CLI:
// https://github.com/hashicorp/vault/blob/7aa1ffa92ee61b977efad1488b8f309b1e2136df/command/kv_get.go#L94-L110
func (p *plugin) rewritePath(path string) (bool, string) {
r := p.client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path)
resp, err := p.client.RawRequest(r)
if err != nil {
logrus.Debugf("failed querying mount point; defaulting to original: %v", err)
return false, path
}
defer resp.Body.Close()
isV2, rewritten, err := rewritePath(resp.Body, path)
if err != nil {
logrus.Debugf("failed rewriting; defaulting to original: %v", err)
return false, path
}
logrus.Debugf("rewrote %q to %q", path, rewritten)
return isV2, rewritten
}

func rewritePath(r io.Reader, original string) (isV2 bool, rewritten string, _ error) {
defer func() {
// never permit a trailing slash, no matter what user puts in
rewritten = strings.TrimSuffix(rewritten, "/")
}()
/*
Example v2 response:
{
"request_id": "4a3a3ef6-d0a8-9a9b-d7eb-c320ef170b55",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"accessor": "kv_f055aa7b",
"config": {
"default_lease_ttl": 0,
"force_no_cache": false,
"max_lease_ttl": 0
},
"description": "versioned encrypted key/value storage",
"local": false,
"options": {
"version": "2"
},
"path": "foo/versioned/",
"seal_wrap": false,
"type": "kv",
"uuid": "eb3b578c-a0bf-2a91-19dc-4155e8ae0116"
},
"wrap_info": null,
"warnings": null,
"auth": null
}
*/
var response struct {
Data struct {
Options struct {
Version string `json:"version"`
} `json:"options"`
Path string `json:"path"`
} `json:"data"`
}
if err := json.NewDecoder(r).Decode(&response); err != nil {
return false, original, fmt.Errorf("failed parsing response: %v", err)
}
v, err := strconv.Atoi(response.Data.Options.Version)
if err != nil || v != 2 {
return false, original, nil // we only rewrite v2
}

mountPath := response.Data.Path
if original == mountPath || original == strings.TrimSuffix(mountPath, "/") {
return true, path.Join(mountPath, "data"), nil
}

return true, path.Join(mountPath, "data", strings.TrimPrefix(original, mountPath)), nil
}
66 changes: 66 additions & 0 deletions plugin/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ package plugin

import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/drone/drone-go/drone"
Expand Down Expand Up @@ -242,3 +245,66 @@ func TestPlugin_KeyNotFound(t *testing.T) {
return
}
}

/* Test_rewritePath establishes that the behavior of our path rewrite logic exactly parallels that of the Vault CLI;
generated with logic like (requires authentication to a vault namespace with both v1 and v2 engines mounted):
main() {
for path in mount{/v2{,/data},/v1}{/bar,}{,/}; do
jq --null-input \
--arg mount_data "$(get_mount "${path}")" \
--arg rewritten "$(get_rewritten_path "${path}")" \
--arg path "${path}" \
'{$mount_data, $rewritten, $path}'
done |
jq -s '[.[] | (.is_v2 = (.mount_data | fromjson).data.options.version == "2")]'
}
get_mount() {
local path="${1}"
curl \
--silent \
-H "X-Vault-Request: true" \
-H "X-Vault-Token: $(vault print token)" \
"https://vault.example.com/v1/sys/internal/ui/mounts/${path}"
}
get_rewritten_path() {
local path="${1}"
vault kv get -output-curl-string "${path}" | cut -d/ -f5-
}
main
*/
func Test_rewritePath(t *testing.T) {
var testCases []struct {
Path string `json:"path"`
MountData string `json:"mount_data"`
Rewritten string `json:"rewritten"`
IsV2 bool `json:"is_v2"`
}
func() {
f, err := os.Open("testdata/v2.json")
if err != nil {
t.Skipf("expected test data present at testdata/v2.json: %v", err)
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&testCases); err != nil {
t.Fatalf("malformed test data: %v", err)
}
}()
for _, tc := range testCases {
t.Run(strings.ReplaceAll(tc.Path, "/", "_"), func(t *testing.T) {
isV2, rewrite, err := rewritePath(strings.NewReader(tc.MountData), tc.Path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if rewrite != tc.Rewritten {
t.Errorf("expected %q but got %q", tc.Rewritten, rewrite)
}
if isV2 != tc.IsV2 {
t.Errorf("expected %v but got %v", tc.IsV2, isV2)
}
})
}
}
34 changes: 34 additions & 0 deletions plugin/testdata/generate_v2_json.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash

# assumes the user has access to s a PREFIX with a V1 mount and a V2 mount

readonly _PREFIX="${PREFIX:-mount}"
readonly _V2="${V2_MOUNT:-/v2}"
readonly _V1="${V1_MOUNT:-/v1}"

main() {
for path in $_PREFIX{$_V2{,/data},$_V1}{/bar,}{,/}; do
jq --null-input \
--arg mount_data "$(get_mount "${path}")" \
--arg rewritten "$(get_rewritten_path "${path}")" \
--arg path "${path}" \
'{$mount_data, $rewritten, $path}'
done |
jq -s '[.[] | (.is_v2 = (.mount_data | fromjson).data.options.version == "2")]'
}

get_mount() {
local path="${1}"
curl \
--silent \
-H "X-Vault-Request: true" \
-H "X-Vault-Token: $(vault print token)" \
"https://vault.example.com/v1/sys/internal/ui/mounts/${path}"
}

get_rewritten_path() {
local path="${1}"
vault kv get -output-curl-string "${path}" | cut -d/ -f5-
}

main
74 changes: 74 additions & 0 deletions plugin/testdata/v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
[
{
"mount_data": "{\"request_id\":\"b2f6799d-7a1d-8bdc-ac5f-0c1238c17969\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"kv_f055aa7b\",\"config\":{\"default_lease_ttl\":0,\"force_no_cache\":false,\"max_lease_ttl\":0},\"description\":\"versioned encrypted key/value storage\",\"local\":false,\"options\":{\"version\":\"2\"},\"path\":\"mount/v2/\",\"seal_wrap\":false,\"type\":\"kv\",\"uuid\":\"eb3b578c-a0bf-2a91-19dc-4155e8ae0116\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v2/data/bar",
"path": "mount/v2/bar",
"is_v2": true
},
{
"mount_data": "{\"request_id\":\"85d5a8e6-348c-a9e5-7cf8-b0fce5d3c3af\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"kv_f055aa7b\",\"config\":{\"default_lease_ttl\":0,\"force_no_cache\":false,\"max_lease_ttl\":0},\"description\":\"versioned encrypted key/value storage\",\"local\":false,\"options\":{\"version\":\"2\"},\"path\":\"mount/v2/\",\"seal_wrap\":false,\"type\":\"kv\",\"uuid\":\"eb3b578c-a0bf-2a91-19dc-4155e8ae0116\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v2/data/bar",
"path": "mount/v2/bar/",
"is_v2": true
},
{
"mount_data": "{\"request_id\":\"db9ca87a-f7a1-70f0-89aa-8ca985483793\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"kv_f055aa7b\",\"config\":{\"default_lease_ttl\":0,\"force_no_cache\":false,\"max_lease_ttl\":0},\"description\":\"versioned encrypted key/value storage\",\"local\":false,\"options\":{\"version\":\"2\"},\"path\":\"mount/v2/\",\"seal_wrap\":false,\"type\":\"kv\",\"uuid\":\"eb3b578c-a0bf-2a91-19dc-4155e8ae0116\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v2/data",
"path": "mount/v2",
"is_v2": true
},
{
"mount_data": "{\"request_id\":\"2c1e9144-af1b-41a4-1c3e-980e13f0e937\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"kv_f055aa7b\",\"config\":{\"default_lease_ttl\":0,\"force_no_cache\":false,\"max_lease_ttl\":0},\"description\":\"versioned encrypted key/value storage\",\"local\":false,\"options\":{\"version\":\"2\"},\"path\":\"mount/v2/\",\"seal_wrap\":false,\"type\":\"kv\",\"uuid\":\"eb3b578c-a0bf-2a91-19dc-4155e8ae0116\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v2/data",
"path": "mount/v2/",
"is_v2": true
},
{
"mount_data": "{\"request_id\":\"ccb9fbd3-ed2c-eb98-f8e6-0f5fcb570f57\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"kv_f055aa7b\",\"config\":{\"default_lease_ttl\":0,\"force_no_cache\":false,\"max_lease_ttl\":0},\"description\":\"versioned encrypted key/value storage\",\"local\":false,\"options\":{\"version\":\"2\"},\"path\":\"mount/v2/\",\"seal_wrap\":false,\"type\":\"kv\",\"uuid\":\"eb3b578c-a0bf-2a91-19dc-4155e8ae0116\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v2/data/data/bar",
"path": "mount/v2/data/bar",
"is_v2": true
},
{
"mount_data": "{\"request_id\":\"a6d671b1-9029-95ac-5560-846345443f81\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"kv_f055aa7b\",\"config\":{\"default_lease_ttl\":0,\"force_no_cache\":false,\"max_lease_ttl\":0},\"description\":\"versioned encrypted key/value storage\",\"local\":false,\"options\":{\"version\":\"2\"},\"path\":\"mount/v2/\",\"seal_wrap\":false,\"type\":\"kv\",\"uuid\":\"eb3b578c-a0bf-2a91-19dc-4155e8ae0116\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v2/data/data/bar",
"path": "mount/v2/data/bar/",
"is_v2": true
},
{
"mount_data": "{\"request_id\":\"23fba99b-f552-6ec5-a0d8-f6ef41d4e4c0\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"kv_f055aa7b\",\"config\":{\"default_lease_ttl\":0,\"force_no_cache\":false,\"max_lease_ttl\":0},\"description\":\"versioned encrypted key/value storage\",\"local\":false,\"options\":{\"version\":\"2\"},\"path\":\"mount/v2/\",\"seal_wrap\":false,\"type\":\"kv\",\"uuid\":\"eb3b578c-a0bf-2a91-19dc-4155e8ae0116\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v2/data/data",
"path": "mount/v2/data",
"is_v2": true
},
{
"mount_data": "{\"request_id\":\"b2d1501e-f748-4c71-0258-27b36aa95f2b\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"kv_f055aa7b\",\"config\":{\"default_lease_ttl\":0,\"force_no_cache\":false,\"max_lease_ttl\":0},\"description\":\"versioned encrypted key/value storage\",\"local\":false,\"options\":{\"version\":\"2\"},\"path\":\"mount/v2/\",\"seal_wrap\":false,\"type\":\"kv\",\"uuid\":\"eb3b578c-a0bf-2a91-19dc-4155e8ae0116\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v2/data/data",
"path": "mount/v2/data/",
"is_v2": true
},
{
"mount_data": "{\"request_id\":\"253551b3-a01e-1126-0dee-f5fd3470b4a4\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"generic_c45ac6cc\",\"config\":{\"default_lease_ttl\":86400,\"force_no_cache\":false,\"max_lease_ttl\":604800},\"description\":\"encrypted key/value storage\",\"local\":false,\"options\":{},\"path\":\"mount/v1/\",\"seal_wrap\":false,\"type\":\"generic\",\"uuid\":\"b0c47310-507d-e5d6-f186-03d3aa71cb58\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v1/bar",
"path": "mount/v1/bar",
"is_v2": false
},
{
"mount_data": "{\"request_id\":\"1f5f8620-cdb3-0aa1-81f3-4308e3e5fd6a\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"generic_c45ac6cc\",\"config\":{\"default_lease_ttl\":86400,\"force_no_cache\":false,\"max_lease_ttl\":604800},\"description\":\"encrypted key/value storage\",\"local\":false,\"options\":{},\"path\":\"mount/v1/\",\"seal_wrap\":false,\"type\":\"generic\",\"uuid\":\"b0c47310-507d-e5d6-f186-03d3aa71cb58\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v1/bar",
"path": "mount/v1/bar/",
"is_v2": false
},
{
"mount_data": "{\"request_id\":\"a9ed61f0-fc96-3f5e-188a-c52afcb8341d\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"generic_c45ac6cc\",\"config\":{\"default_lease_ttl\":86400,\"force_no_cache\":false,\"max_lease_ttl\":604800},\"description\":\"encrypted key/value storage\",\"local\":false,\"options\":{},\"path\":\"mount/v1/\",\"seal_wrap\":false,\"type\":\"generic\",\"uuid\":\"b0c47310-507d-e5d6-f186-03d3aa71cb58\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v1",
"path": "mount/v1",
"is_v2": false
},
{
"mount_data": "{\"request_id\":\"fe545ea3-06a5-51d3-0236-4982e896cf8f\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"accessor\":\"generic_c45ac6cc\",\"config\":{\"default_lease_ttl\":86400,\"force_no_cache\":false,\"max_lease_ttl\":604800},\"description\":\"encrypted key/value storage\",\"local\":false,\"options\":{},\"path\":\"mount/v1/\",\"seal_wrap\":false,\"type\":\"generic\",\"uuid\":\"b0c47310-507d-e5d6-f186-03d3aa71cb58\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}",
"rewritten": "mount/v1",
"path": "mount/v1/",
"is_v2": false
}
]

0 comments on commit 17add00

Please sign in to comment.