mirror of https://github.com/hashicorp/boundary
feat(credential/vault): Implement UPD vault credential extractor (#1520)
parent
34b4028d31
commit
ee908ddc23
@ -0,0 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
// Package usernamepassworddomain provides access to the username, password, and domain
|
||||
// stored in a Vault secret.
|
||||
package usernamepassworddomain
|
||||
@ -0,0 +1,161 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package usernamepassworddomain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/pointerstructure"
|
||||
)
|
||||
|
||||
type (
|
||||
data map[string]any
|
||||
|
||||
// extractFunc attempts to extract the username, password, and domain
|
||||
// from sd using the provided attribute names, using a known
|
||||
// Vault data response format.
|
||||
extractFunc func(sd data, usernameAttr, passwordAttr, domainAttr string) (string, string, string)
|
||||
)
|
||||
|
||||
// Extract attempts to extract the values of the username and password
|
||||
// stored within the provided data using the given attribute names.
|
||||
//
|
||||
// Extract does not return partial results, i.e. if one of the attributes
|
||||
// were extracted but not the other ("", "") will be returned.
|
||||
func Extract(d data, usernameAttr, passwordAttr, domainAttr string) (string, string, string) {
|
||||
for _, f := range []extractFunc{
|
||||
defaultExtract,
|
||||
kv2Extract,
|
||||
} {
|
||||
username, password, domain := f(d, usernameAttr, passwordAttr, domainAttr)
|
||||
if username != "" && password != "" && domain != "" {
|
||||
// got valid username and password from secret
|
||||
return username, password, domain
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
// defaultExtract looks for the usernameAttr, passwordAttr, and domainAttr in the data map
|
||||
func defaultExtract(sd data, usernameAttr, passwordAttr, domainAttr string) (username string, password string, domain string) {
|
||||
if sd == nil {
|
||||
// nothing to do return early
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
var u any
|
||||
switch {
|
||||
case strings.HasPrefix(usernameAttr, "/"):
|
||||
var err error
|
||||
u, err = pointerstructure.Get(sd, usernameAttr)
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
default:
|
||||
u = sd[usernameAttr]
|
||||
}
|
||||
if u, ok := u.(string); ok {
|
||||
username = u
|
||||
}
|
||||
|
||||
var p any
|
||||
switch {
|
||||
case strings.HasPrefix(passwordAttr, "/"):
|
||||
var err error
|
||||
p, err = pointerstructure.Get(sd, passwordAttr)
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
default:
|
||||
p = sd[passwordAttr]
|
||||
}
|
||||
|
||||
if p, ok := p.(string); ok {
|
||||
password = p
|
||||
}
|
||||
|
||||
var d any
|
||||
switch {
|
||||
case strings.HasPrefix(domainAttr, "/"):
|
||||
var err error
|
||||
d, err = pointerstructure.Get(sd, domainAttr)
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
default:
|
||||
d = sd[domainAttr]
|
||||
}
|
||||
if d, ok := d.(string); ok {
|
||||
domain = d
|
||||
}
|
||||
|
||||
return username, password, domain
|
||||
}
|
||||
|
||||
// kv2Extract looks for the usernameAttr, passwordAttr, and domainAttr in the embedded
|
||||
// 'data' field within the data map.
|
||||
//
|
||||
// Additionally it validates the data is in the expected KV-v2 format:
|
||||
//
|
||||
// {
|
||||
// "data": {},
|
||||
// "metadata: {}
|
||||
// }
|
||||
//
|
||||
// If the format does not match, it returns ("", "", ""). See:
|
||||
// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1
|
||||
func kv2Extract(sd data, usernameAttr, passwordAttr, domainAttr string) (username string, password string, domain string) {
|
||||
if sd == nil {
|
||||
// nothing to do return early
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
var data, metadata map[string]any
|
||||
for k, v := range sd {
|
||||
switch k {
|
||||
case "data":
|
||||
var ok bool
|
||||
if data, ok = v.(map[string]any); !ok {
|
||||
// data field should be of type map[string]interface{} in KV-v2
|
||||
return "", "", ""
|
||||
}
|
||||
case "metadata":
|
||||
var ok bool
|
||||
if metadata, ok = v.(map[string]any); !ok {
|
||||
// metadata field should be of type map[string]interface{} in KV-v2
|
||||
return "", "", ""
|
||||
}
|
||||
default:
|
||||
// secretData contains a non valid KV-v2 top level field
|
||||
return "", "", ""
|
||||
}
|
||||
}
|
||||
if data == nil || metadata == nil {
|
||||
// missing required KV-v2 field
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
if u, ok := data[usernameAttr]; ok {
|
||||
if u, ok := u.(string); ok {
|
||||
username = u
|
||||
}
|
||||
}
|
||||
if p, ok := data[passwordAttr]; ok {
|
||||
if p, ok := p.(string); ok {
|
||||
password = p
|
||||
}
|
||||
}
|
||||
|
||||
if d, ok := data[domainAttr]; ok {
|
||||
if d, ok := d.(string); ok {
|
||||
domain = d
|
||||
}
|
||||
}
|
||||
|
||||
return username, password, domain
|
||||
}
|
||||
@ -0,0 +1,325 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package usernamepassworddomain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBaseToUsrPassDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
s data
|
||||
uAttr string
|
||||
pAttr string
|
||||
dAttr string
|
||||
}
|
||||
type usrPassDomain struct {
|
||||
user string
|
||||
pass string
|
||||
domain string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
given args
|
||||
want usrPassDomain
|
||||
}{
|
||||
{
|
||||
name: "nil-input",
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "no-input",
|
||||
given: args{},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "no-secret",
|
||||
given: args{
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "no-match-username-secret",
|
||||
given: args{
|
||||
s: data{
|
||||
"username-wrong": "user",
|
||||
"password": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "no-match-password-secret",
|
||||
given: args{
|
||||
s: data{
|
||||
"username": "user",
|
||||
"password-wrong": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "no-match-domain-secret",
|
||||
given: args{
|
||||
s: data{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"domain-wrong": "domain",
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "valid-default",
|
||||
given: args{
|
||||
s: data{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "user", pass: "pass", domain: "domain"},
|
||||
},
|
||||
{
|
||||
name: "no-match-username-secret-kv2",
|
||||
given: args{
|
||||
s: data{
|
||||
"metadata": map[string]any{},
|
||||
"data": map[string]any{
|
||||
"username-wrong": "user",
|
||||
"password": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "no-match-password-secret-kv2",
|
||||
given: args{
|
||||
s: data{
|
||||
"metadata": map[string]any{},
|
||||
"data": map[string]any{
|
||||
"username": "user",
|
||||
"password-wrong": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "no-match-domain-secret-kv2",
|
||||
given: args{
|
||||
s: data{
|
||||
"metadata": map[string]any{},
|
||||
"data": map[string]any{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"domain-wrong": "domain",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "valid-kv2",
|
||||
given: args{
|
||||
s: data{
|
||||
"metadata": map[string]any{},
|
||||
"data": map[string]any{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "user", pass: "pass", domain: "domain"},
|
||||
},
|
||||
{
|
||||
name: "no-metadata-kv2",
|
||||
given: args{
|
||||
s: data{
|
||||
"data": map[string]any{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "invalid-metadata-kv2",
|
||||
given: args{
|
||||
s: data{
|
||||
"metadata": "string",
|
||||
"data": map[string]any{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "invalid-field-kv2",
|
||||
given: args{
|
||||
s: data{
|
||||
"invalid": map[string]any{},
|
||||
"metadata": map[string]any{},
|
||||
"data": map[string]any{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"domain": "domain",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "", pass: "", domain: ""},
|
||||
},
|
||||
{
|
||||
name: "valid-order-default-first",
|
||||
given: args{
|
||||
s: data{
|
||||
"username": "default-user",
|
||||
"password": "default-pass",
|
||||
"domain": "default-domain",
|
||||
"metadata": map[string]any{},
|
||||
"data": map[string]any{
|
||||
"username": "kv2-user",
|
||||
"password": "kv2-pass",
|
||||
"domain": "kv2-domain",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "default-user", pass: "default-pass", domain: "default-domain"},
|
||||
},
|
||||
{
|
||||
name: "default-user-json-pointer-password",
|
||||
given: args{
|
||||
s: data{
|
||||
"username": "default-user",
|
||||
"domain": "default-domain",
|
||||
"testing": map[string]any{
|
||||
"my-password": "secret",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "/testing/my-password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "default-user", pass: "secret", domain: "default-domain"},
|
||||
},
|
||||
{
|
||||
name: "default-pk-json-pointer-user",
|
||||
given: args{
|
||||
s: data{
|
||||
"password": "default-pass",
|
||||
"domain": "default-domain",
|
||||
"testing": map[string]any{
|
||||
"a-user-name": "me",
|
||||
},
|
||||
},
|
||||
uAttr: "/testing/a-user-name",
|
||||
pAttr: "password",
|
||||
dAttr: "domain",
|
||||
},
|
||||
want: usrPassDomain{user: "me", pass: "default-pass", domain: "default-domain"},
|
||||
},
|
||||
{
|
||||
name: "default-dm-json-pointer-user",
|
||||
given: args{
|
||||
s: data{
|
||||
"username": "default-user",
|
||||
"password": "default-pass",
|
||||
"domain-site": map[string]any{
|
||||
"a-domain": "domain.com",
|
||||
},
|
||||
},
|
||||
uAttr: "username",
|
||||
pAttr: "password",
|
||||
dAttr: "/domain-site/a-domain",
|
||||
},
|
||||
want: usrPassDomain{user: "default-user", pass: "default-pass", domain: "domain.com"},
|
||||
},
|
||||
{
|
||||
name: "all-json-pointer",
|
||||
given: args{
|
||||
s: data{
|
||||
"first-path": map[string]any{
|
||||
"deeper-path": map[string]any{
|
||||
"my-special-user": "you-found-me",
|
||||
},
|
||||
},
|
||||
"testing": map[string]any{
|
||||
"password": "secret",
|
||||
},
|
||||
"domain-site": map[string]any{
|
||||
"a-domain": "domain.com",
|
||||
},
|
||||
},
|
||||
uAttr: "/first-path/deeper-path/my-special-user",
|
||||
pAttr: "/testing/password",
|
||||
dAttr: "/domain-site/a-domain",
|
||||
},
|
||||
want: usrPassDomain{user: "you-found-me", pass: "secret", domain: "domain.com"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
user, pass, domain := Extract(tt.given.s, tt.given.uAttr, tt.given.pAttr, tt.given.dAttr)
|
||||
assert.Equal(tt.want.user, user)
|
||||
assert.Equal(tt.want.pass, pass)
|
||||
assert.Equal(tt.want.domain, domain)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue