Add initial templating support to Vault credential libraries (#2569) (#2575)

Add initial templating support to Vault credential libraries

This adds support for templating various account and user values into
Vault dynamic credential library POST bodies and GET/POST paths.

It also unifies how parameters are used in grants, with backwards
compatibility.
This commit is contained in:
Jeff Mitchell authored and GitHub committed 2022-10-27 09:01:05 -04:00
1 parent b5ef8a516b
commit cede1aec93
25 files changed
+612 -78

No files matched your search

+145 -46
View File
@@ -10,13 +10,15 @@ import (
"github.com/hashicorp/boundary/api/recovery"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/errors"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
"github.com/hashicorp/boundary/internal/gen/controller/tokens"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/perms"
@@ -25,6 +27,9 @@ import (
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/internal/types/subtypes"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/boundary/internal/util/template"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/mr-tron/base58"
@@ -56,6 +61,9 @@ type key int
var verifierKey key
type VerifyResults struct {
UserData template.Data
// This is copied out from UserData above but used to avoid nil checks in
// lots of places that embed this value
UserId string
AuthTokenId string
Error error
@@ -92,15 +100,47 @@ type VerifyResults struct {
}
type verifier struct {
iamRepoFn common.IamRepoFactory
authTokenRepoFn common.AuthTokenRepoFactory
serversRepoFn common.ServersRepoFactory
kms *kms.Kms
requestInfo *authpb.RequestInfo
res *perms.Resource
act action.Type
ctx context.Context
acl perms.ACL
iamRepoFn common.IamRepoFactory
authTokenRepoFn common.AuthTokenRepoFactory
serversRepoFn common.ServersRepoFactory
passwordAuthRepoFn common.PasswordAuthRepoFactory
oidcAuthRepoFn common.OidcAuthRepoFactory
kms *kms.Kms
requestInfo *authpb.RequestInfo
res *perms.Resource
act action.Type
ctx context.Context
acl perms.ACL
}
// TODO (jefferai 10/2022): NewVerifierContextWithAccounts performs the function
// of NewVerifierContext (see the docs for that function) but with extra
// parameters that can be used to look up account information. This is not
// intended to be a long-lived function; see
// https://hashicorp.atlassian.net/browse/ICU-6571 and
// https://hashicorp.atlassian.net/browse/ICU-6572
//
// This is being added for a quick turnaround purpose and to avoid making large
// numbers of changes to tests when we may do a much bigger refactor; when those
// items are addressed this can be removed.
func NewVerifierContextWithAccounts(ctx context.Context,
iamRepoFn common.IamRepoFactory,
authTokenRepoFn common.AuthTokenRepoFactory,
serversRepoFn common.ServersRepoFactory,
passwordAuthRepoFn common.PasswordAuthRepoFactory,
oidcAuthRepoFn common.OidcAuthRepoFactory,
kms *kms.Kms,
requestInfo *authpb.RequestInfo,
) context.Context {
return context.WithValue(ctx, verifierKey, &verifier{
iamRepoFn: iamRepoFn,
authTokenRepoFn: authTokenRepoFn,
serversRepoFn: serversRepoFn,
passwordAuthRepoFn: passwordAuthRepoFn,
oidcAuthRepoFn: oidcAuthRepoFn,
kms: kms,
requestInfo: requestInfo,
})
}
// NewVerifierContext creates a context that carries a verifier object from the
@@ -114,13 +154,7 @@ func NewVerifierContext(ctx context.Context,
kms *kms.Kms,
requestInfo *authpb.RequestInfo,
) context.Context {
return context.WithValue(ctx, verifierKey, &verifier{
iamRepoFn: iamRepoFn,
authTokenRepoFn: authTokenRepoFn,
serversRepoFn: serversRepoFn,
kms: kms,
requestInfo: requestInfo,
})
return NewVerifierContextWithAccounts(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, nil, nil, kms, requestInfo)
}
// Verify takes in a context that has expected parameters as values and runs an
@@ -209,6 +243,7 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
}
}
ret.UserId = v.requestInfo.UserIdOverride
ret.UserData.User.Id = util.Pointer(ret.UserId)
if reqInfo != nil {
reqInfo.UserId = ret.UserId
}
@@ -235,14 +270,17 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
var authResults perms.ACLResults
var grantTuples []perms.GrantTuple
var accountId, userName, userEmail string
var userData template.Data
var err error
authResults, ret.UserId, accountId, userName, userEmail, ret.Scope, v.acl, grantTuples, err = v.performAuthCheck(ctx)
authResults, ret.UserData, ret.Scope, v.acl, grantTuples, err = v.performAuthCheck(ctx)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error performing authn/authz check"))
return
}
if ret.UserData.User.Id != nil {
ret.UserId = *ret.UserData.User.Id
}
ret.AuthTokenId = v.requestInfo.PublicId
ret.AuthenticationFinished = authResults.AuthenticationFinished
if !authResults.Authorized {
@@ -288,14 +326,20 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
})
}
ea.UserInfo = &event.UserInfo{
UserId: ret.UserId,
AuthAccountId: accountId,
UserId: ret.UserId,
}
if userData.Account.Id != nil {
ea.UserInfo.AuthAccountId = *userData.Account.Id
}
ea.GrantsInfo = &event.GrantsInfo{
Grants: grants,
}
ea.UserName = userName
ea.UserEmail = userEmail
if userData.User.FullName != nil {
ea.UserName = *userData.User.FullName
}
if userData.User.Email != nil {
ea.UserEmail = *userData.User.Email
}
if reqInfo != nil {
reqInfo.UserId = ret.UserId
@@ -434,10 +478,7 @@ func (v *verifier) decryptToken(ctx context.Context) {
func (v verifier) performAuthCheck(ctx context.Context) (
aclResults perms.ACLResults,
userId string,
accountId string,
userName string,
userEmail string,
userData template.Data,
scopeInfo *scopes.ScopeInfo,
retAcl perms.ACL,
grantTuples []perms.GrantTuple,
@@ -449,7 +490,10 @@ func (v verifier) performAuthCheck(ctx context.Context) (
// Make the linter happy
_ = retErr
scopeInfo = new(scopes.ScopeInfo)
userId = AnonymousUserId
// This will always be set, so further down below we can switch on whether
// it's empty
userData.User.Id = util.Pointer(AnonymousUserId)
// Validate the token and fetch the corresponding user ID
switch v.requestInfo.TokenFormat {
@@ -459,7 +503,7 @@ func (v verifier) performAuthCheck(ctx context.Context) (
case uint32(AuthTokenTypeRecoveryKms):
// We validated the encrypted token in decryptToken and handled the
// nonces there, so just set the user
userId = "u_recovery"
userData.User.Id = util.Pointer("u_recovery")
case uint32(AuthTokenTypeBearer), uint32(AuthTokenTypeSplitCookie):
if v.requestInfo.Token == "" {
@@ -479,12 +523,12 @@ func (v verifier) performAuthCheck(ctx context.Context) (
break
}
if at != nil {
accountId = at.GetAuthAccountId()
userId = at.GetIamUserId()
if userId == "" {
userData.Account.Id = util.Pointer(at.GetAuthAccountId())
userData.User.Id = util.Pointer(at.GetIamUserId())
if *userData.User.Id == "" {
event.WriteError(ctx, op, stderrors.New("perform auth check: valid token did not map to a user, likely because no account is associated with the user any longer; continuing as u_anon"), event.WithInfo("token_id", at.GetPublicId()))
userId = AnonymousUserId
accountId = ""
userData.User.Id = util.Pointer(AnonymousUserId)
userData.Account.Id = nil
}
}
}
@@ -495,13 +539,51 @@ func (v verifier) performAuthCheck(ctx context.Context) (
return
}
u, _, err := iamRepo.LookupUser(ctx, userId)
u, _, err := iamRepo.LookupUser(ctx, *userData.User.Id)
if err != nil {
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("failed to lookup user"))
return
}
userEmail = u.Email
userName = u.FullName
userData.User.Name = util.Pointer(u.Name)
userData.User.Email = util.Pointer(u.Email)
userData.User.FullName = util.Pointer(u.FullName)
if userData.Account.Id != nil && *userData.Account.Id != "" && v.passwordAuthRepoFn != nil && v.oidcAuthRepoFn != nil {
const domain = "auth"
var acct auth.Account
var err error
switch subtypes.SubtypeFromId(domain, *userData.Account.Id) {
case password.Subtype:
repo, repoErr := v.passwordAuthRepoFn()
if repoErr != nil {
retErr = errors.Wrap(ctx, repoErr, op, errors.WithMsg("failed to get password auth repo"))
return
}
acct, err = repo.LookupAccount(ctx, *userData.Account.Id)
case oidc.Subtype:
repo, repoErr := v.oidcAuthRepoFn()
if repoErr != nil {
retErr = errors.Wrap(ctx, repoErr, op, errors.WithMsg("failed to get oidc auth repo"))
return
}
acct, err = repo.LookupAccount(ctx, *userData.Account.Id)
default:
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("unrecognized account id type"))
return
}
if err != nil {
if errors.IsNotFoundError(err) {
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("account doesn't exist"))
return
}
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("error looking up account"))
return
}
userData.Account.Name = util.Pointer(acct.GetName())
userData.Account.Email = util.Pointer(acct.GetEmail())
userData.Account.LoginName = util.Pointer(acct.GetLoginName())
userData.Account.Subject = util.Pointer(acct.GetSubject())
}
// Look up scope details to return. We can skip a lookup when using the
// global scope
@@ -546,7 +628,7 @@ func (v verifier) performAuthCheck(ctx context.Context) (
// Fetch and parse grants for this user ID (which may include grants for
// u_anon and u_auth)
grantTuples, err = iamRepo.GrantsForUser(v.ctx, userId)
grantTuples, err = iamRepo.GrantsForUser(v.ctx, *userData.User.Id)
if err != nil {
retErr = errors.Wrap(ctx, err, op)
return
@@ -556,12 +638,17 @@ func (v verifier) performAuthCheck(ctx context.Context) (
// that we've since restricted, e.g. "id=foo;actions=create,read". These
// will simply not have an effect.
for _, pair := range grantTuples {
permsOpts := []perms.Option{
perms.WithUserId(*userData.User.Id),
perms.WithSkipFinalValidation(true),
}
if userData.Account.Id != nil {
permsOpts = append(permsOpts, perms.WithAccountId(*userData.Account.Id))
}
parsed, err := perms.Parse(
pair.ScopeId,
pair.Grant,
perms.WithUserId(userId),
perms.WithAccountId(accountId),
perms.WithSkipFinalValidation(true))
permsOpts...)
if err != nil {
retErr = errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to parse grant %#v", pair.Grant)))
return
@@ -570,7 +657,7 @@ func (v verifier) performAuthCheck(ctx context.Context) (
}
retAcl = perms.NewACL(parsedGrants...)
aclResults = retAcl.Allowed(*v.res, v.act, userId)
aclResults = retAcl.Allowed(*v.res, v.act, *userData.User.Id)
// We don't set authenticated above because setting this but not authorized
// is used for further permissions checks, such as during recursive listing.
// So we want to make sure any code relying on that has the full set of
@@ -601,6 +688,13 @@ func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActi
return availableActions
}
// If there is no user ID set by definition there are no actions to fetch.
// This shouldn't happen because we should always fall back to at least the
// anonymous user so it's defense in depth.
if r.UserData.User.Id == nil {
return nil
}
opts := getOpts(opt...)
res := opts.withResource
// If not passed in, use what's already been populated through verification
@@ -620,7 +714,7 @@ func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActi
ret := make(action.ActionSet, 0, len(availableActions))
for _, act := range availableActions {
if r.v.acl.Allowed(*res, act, r.UserId).Authorized {
if r.v.acl.Allowed(*res, act, *r.UserData.User.Id).Authorized {
ret = append(ret, act)
}
}
@@ -636,9 +730,14 @@ func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) p
return perms.OutputFieldsMap{"*": true}
case r.v.requestInfo.DisableAuthEntirely:
return nil
case r.UserData.User.Id == nil:
// If there is no user ID set by definition there are no actions to fetch.
// This shouldn't happen because we should always fall back to at least the
// anonymous user so it's defense in depth.
return nil
}
return r.v.acl.Allowed(res, act, r.UserId).OutputFields
return r.v.acl.Allowed(res, act, *r.UserData.User.Id).OutputFields
}
// ACL returns the perms.ACL of the verifier.
+3 -1
View File
@@ -57,6 +57,8 @@ func newGrpcServer(
iamRepoFn common.IamRepoFactory,
authTokenRepoFn common.AuthTokenRepoFactory,
serversRepoFn common.ServersRepoFactory,
passwordAuthRepoFn common.PasswordAuthRepoFactory,
oidcAuthRepoFn common.OidcAuthRepoFactory,
kms *kms.Kms,
eventer *event.Eventer,
) (*grpc.Server, string, error) {
@@ -65,7 +67,7 @@ func newGrpcServer(
if err != nil {
return nil, "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate gateway ticket"))
}
requestCtxInterceptor, err := requestCtxInterceptor(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, kms, ticket, eventer)
requestCtxInterceptor, err := requestCtxInterceptor(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, kms, ticket, eventer)
if err != nil {
return nil, "", err
}
@@ -853,7 +853,7 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
dynamic, err = credRepo.Issue(ctx, sess.GetPublicId(), vaultReqs)
dynamic, err = credRepo.Issue(ctx, sess.GetPublicId(), vaultReqs, credential.WithTemplateData(authResults.UserData))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
@@ -2,8 +2,10 @@ package tcp_test
import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"path"
@@ -11,6 +13,8 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/credential"
credstatic "github.com/hashicorp/boundary/internal/credential/static"
@@ -2260,6 +2264,12 @@ func TestAuthorizeSession(t *testing.T) {
atRepoFn := func() (*authtoken.Repository, error) {
return authtoken.NewRepository(rw, rw, kms)
}
passwordAuthRepoFn := func() (*password.Repository, error) {
return password.NewRepository(rw, rw, kms)
}
oidcAuthRepoFn := func() (*oidc.Repository, error) {
return oidc.NewRepository(ctx, rw, rw, kms)
}
plg := host.TestPlugin(t, conn, "test")
plgm := map[string]plgpb.HostPluginServiceClient{
@@ -2290,12 +2300,23 @@ func TestAuthorizeSession(t *testing.T) {
return plugin.NewRepository(rw, rw, kms, sche, plgm)
}
loginName := "foo@bar.com"
accountName := "passname"
userName := "username"
org, proj := iam.TestScopes(t, iamRepo)
at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId())
ctx = auth.NewVerifierContext(requests.NewRequestContext(ctx),
at := authtoken.TestAuthToken(t,
conn,
kms,
org.GetPublicId(),
authtoken.WithPasswordOptions(password.WithLoginName(loginName), password.WithName(accountName)),
authtoken.WithIamOptions(iam.WithName(userName)))
ctx = auth.NewVerifierContextWithAccounts(requests.NewRequestContext(ctx),
iamRepoFn,
atRepoFn,
serversRepoFn,
passwordAuthRepoFn,
oidcAuthRepoFn,
kms,
&authpb.RequestInfo{
Token: at.GetToken(),
@@ -2327,7 +2348,7 @@ func TestAuthorizeSession(t *testing.T) {
plugin.TestRunSetSync(t, conn, kms, plgm)
v := vault.NewTestVaultServer(t)
v.MountPKI(t)
v.MountPKI(t, vault.WithTestMountPath("pki/"+userName))
sec, tok := v.CreateToken(t, vault.WithPolicies([]string{"default", "boundary-controller", "pki"}))
vaultStore := vault.TestCredentialStore(t, conn, wrapper, proj.GetPublicId(), v.Addr, tok, sec.Auth.Accessor)
@@ -2340,13 +2361,13 @@ func TestAuthorizeSession(t *testing.T) {
Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{
VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{
Path: &wrapperspb.StringValue{
Value: path.Join("pki", "issue", "boundary"),
Value: path.Join("pki/{{ .User.Name}}", "issue", "boundary"),
},
HttpMethod: &wrapperspb.StringValue{
Value: "POST",
},
HttpRequestBody: &wrapperspb.StringValue{
Value: `{"common_name":"boundary.com"}`,
Value: `{"common_name":"boundary.com", "alt_names": "{{.User.Name}},{{.Account.Name}},{{.Account.LoginName}},{{ truncateFrom .Account.LoginName "@" }}"}`,
},
},
},
@@ -2474,6 +2495,16 @@ func TestAuthorizeSession(t *testing.T) {
require.True(t, ok)
assert.Truef(t, strings.HasPrefix(gotV.(string), v.(string)), "%q:%q doesn't have prefix %q", k, gotV, v)
}
b, _ := pem.Decode([]byte(dSec["certificate"].(string)))
require.NotNil(t, b)
cert, err := x509.ParseCertificate(b.Bytes)
require.NoError(t, err)
assert.Contains(t, cert.DNSNames, userName)
assert.Contains(t, cert.DNSNames, accountName)
assert.Contains(t, cert.DNSNames, strings.Split(loginName, "@")[0])
assert.Contains(t, cert.EmailAddresses, loginName)
gotCred.Secret = nil
got.AuthorizationToken, got.SessionId, got.CreatedTime = "", "", nil
@@ -127,7 +127,7 @@ func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs
continue
}
outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId)
outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(*authResults.UserData.User.Id)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields))
if outputFields.Has(globals.ScopeField) {
+3 -1
View File
@@ -48,6 +48,8 @@ func requestCtxInterceptor(
iamRepoFn common.IamRepoFactory,
authTokenRepoFn common.AuthTokenRepoFactory,
serversRepoFn common.ServersRepoFactory,
passwordAuthRepoFn common.PasswordAuthRepoFactory,
oidcAuthRepoFn common.OidcAuthRepoFactory,
kms *kms.Kms,
ticket string,
eventer *event.Eventer,
@@ -105,7 +107,7 @@ func requestCtxInterceptor(
return nil, errors.New(interceptorCtx, errors.Internal, op, "Invalid context (bad ticket)")
}
interceptorCtx = auth.NewVerifierContext(interceptorCtx, iamRepoFn, authTokenRepoFn, serversRepoFn, kms, &requestInfo)
interceptorCtx = auth.NewVerifierContextWithAccounts(interceptorCtx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, kms, &requestInfo)
// Add general request information to the context. The information from
// the auth verifier context is pretty specifically curated to
@@ -311,7 +311,7 @@ func Test_requestCtxInterceptor(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
interceptor, err := requestCtxInterceptor(factoryCtx, tt.iamRepoFn, tt.authTokenRepoFn, tt.serversRepoFn, tt.kms, tt.ticket, tt.eventer)
interceptor, err := requestCtxInterceptor(factoryCtx, tt.iamRepoFn, tt.authTokenRepoFn, tt.serversRepoFn, nil, nil, tt.kms, tt.ticket, tt.eventer)
if tt.wantFactoryErr {
require.Error(err)
assert.Nil(interceptor)
+1 -1
View File
@@ -35,7 +35,7 @@ func closeListener(_ context.Context, l net.Listener, _ any, _ int) error {
func (c *Controller) startListeners() error {
servers := make([]func(), 0, len(c.conf.Listeners))
grpcServer, gwTicket, err := newGrpcServer(c.baseContext, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.kms, c.conf.Eventer)
grpcServer, gwTicket, err := newGrpcServer(c.baseContext, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.PasswordAuthRepoFn, c.OidcRepoFn, c.kms, c.conf.Eventer)
if err != nil {
return fmt.Errorf("failed to create new grpc server: %w", err)
}