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:
1 parent
b5ef8a516b
commit
cede1aec93
25 files changed
+612
-78
No files matched your search
@@ -4,6 +4,33 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
|
||||
|
||||
## Next
|
||||
|
||||
### New and Improved
|
||||
|
||||
* Vault Parameter Templating: In `vault` credential libraries, the paths and any
|
||||
POST bodies can contain templated parameters using Go template syntax (similar
|
||||
to Consul-Template). The following template parameters are supported (note
|
||||
that account values are tied to the account associated with the token making
|
||||
the call):
|
||||
* `{{ .User.Id }}`: the user's ID
|
||||
* `{{ .User.Name }}`: the user's name (from the user resource)
|
||||
* `{{ .User.FullName }}`: the user's name (from the account corresponding to
|
||||
theprimary auth method in the user's scope; this may not be populated or
|
||||
maybe different than the account name in the template)
|
||||
* `{{ .User.Email }}`: the user's email address (same caveat as `FullName`)
|
||||
* `{{ .Account.Id }}`: the account's ID
|
||||
* `{{ .Account.Name }}`: the account's name (from the account resource)
|
||||
* `{{ .Account.LoginName }}`: the account's login name (if used by that type
|
||||
of ccount)
|
||||
* `{{ .Account.Subject }}`: the account's subject (if used by that type
|
||||
of ccount)
|
||||
* `{{ .Account.Email }}`: the account's email (if used by that type
|
||||
of account)
|
||||
|
||||
Additionally, there is currently a single function that strips the rest of a
|
||||
string after a specified substring; this is useful for pulling an user/account name from an email address. In the following example it uses the account email can be any other parameter:
|
||||
|
||||
* `{{ truncateFrom .Account.Email "@" }}`: this would turn `foo@example.com` into `foo`
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* accounts: Deleted auth accounts would still show up as being associated with a
|
||||
@@ -19,6 +46,13 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
|
||||
* workers: Fixed a panic that can happen in certain situations
|
||||
([PR](https://github.com/hashicorp/boundary/pull/2553))
|
||||
|
||||
### Deprecations/Changes
|
||||
|
||||
* In order to standardize on the templating format, [templates in
|
||||
grants](https://developer.hashicorp.com/boundary/docs/concepts/security/permissions/permission-grant-formats#templates)
|
||||
now are documented to use the new capitalization and format; however, the
|
||||
previous style will continue to work.
|
||||
|
||||
## 0.11.0 (2022/09/27)
|
||||
|
||||
### Known Issues
|
||||
|
||||
@@ -52,7 +52,7 @@ func (b *Server) CreateInitialLoginRole(ctx context.Context) (*iam.Role, error)
|
||||
if _, err := iamRepo.AddRoleGrants(ctx, role.PublicId, role.Version, []string{
|
||||
"id=*;type=scope;actions=list,no-op",
|
||||
"id=*;type=auth-method;actions=authenticate,list",
|
||||
"id={{account.id}};actions=read,change-password",
|
||||
"id={{.Account.Id}};actions=read,change-password",
|
||||
"id=*;type=auth-token;actions=list,read:self,delete:self",
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("error creating grant for default generated grants: %w", err)
|
||||
|
||||
@@ -106,7 +106,9 @@ type Issuer interface {
|
||||
//
|
||||
// If Issue encounters an error, it returns no credentials and revokes
|
||||
// any credentials issued before encountering the error.
|
||||
Issue(ctx context.Context, sessionId string, requests []Request) ([]Dynamic, error)
|
||||
//
|
||||
// Supported Options: WithTemplateData
|
||||
Issue(ctx context.Context, sessionId string, requests []Request, opt ...Option) ([]Dynamic, error)
|
||||
}
|
||||
|
||||
// Revoker revokes dynamic credentials.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/util/template"
|
||||
)
|
||||
|
||||
// GetOpts - iterate the inbound Options and return a struct
|
||||
func GetOpts(opt ...Option) (*options, error) {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
if o == nil {
|
||||
continue
|
||||
}
|
||||
if err := o(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments.
|
||||
type Option func(*options) error
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
WithTemplateData template.Data
|
||||
}
|
||||
|
||||
func getDefaultOptions() *options {
|
||||
return &options{}
|
||||
}
|
||||
|
||||
// WithTemplateData provides a way to pass in template information
|
||||
func WithTemplateData(with template.Data) Option {
|
||||
return func(o *options) error {
|
||||
o.WithTemplateData = with
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/boundary/internal/util/template"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("WithTemplateData", func(t *testing.T) {
|
||||
opts := getDefaultOptions()
|
||||
assert.Empty(t, opts.WithTemplateData)
|
||||
opts, err := GetOpts(WithTemplateData(template.Data{User: template.User{Id: util.Pointer("foo")}}))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "foo", *opts.WithTemplateData.User.Id)
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/util/template"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
|
||||
vault "github.com/hashicorp/vault/api"
|
||||
@@ -289,7 +290,14 @@ type dynamicCred interface {
|
||||
|
||||
// retrieveCredential retrieves a dynamic credential from Vault for the
|
||||
// given sessionId.
|
||||
func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, sessionId string) (dynamicCred, error) {
|
||||
//
|
||||
// Supported options: credential.WithTemplateData
|
||||
func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, sessionId string, opt ...credential.Option) (dynamicCred, error) {
|
||||
opts, err := credential.GetOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
// Get the credential ID early. No need to get a secret from Vault
|
||||
// if there is no way to save it in the database.
|
||||
credId, err := newCredentialId()
|
||||
@@ -303,19 +311,47 @@ func (pl *issueCredentialLibrary) retrieveCredential(ctx context.Context, op err
|
||||
}
|
||||
|
||||
var secret *vault.Secret
|
||||
var reqErr error
|
||||
|
||||
// Template the path
|
||||
path := pl.VaultPath
|
||||
if path != "" {
|
||||
parsedTmpl, err := template.New(ctx, path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
path, err = parsedTmpl.Generate(ctx, opts.WithTemplateData)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
|
||||
// Template the body
|
||||
body := string(pl.HttpRequestBody)
|
||||
if body != "" {
|
||||
parsedTmpl, err := template.New(ctx, body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
body, err = parsedTmpl.Generate(ctx, opts.WithTemplateData)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
|
||||
switch Method(pl.HttpMethod) {
|
||||
case MethodGet:
|
||||
secret, err = client.get(ctx, pl.VaultPath)
|
||||
secret, reqErr = client.get(ctx, path)
|
||||
case MethodPost:
|
||||
secret, err = client.post(ctx, pl.VaultPath, pl.HttpRequestBody)
|
||||
secret, reqErr = client.post(ctx, path, []byte(body))
|
||||
default:
|
||||
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unknown http method: library: %s", pl.PublicId))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if reqErr != nil {
|
||||
// TODO(mgaffney) 05/2021: detect if the error is because of an
|
||||
// expired or invalid token
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
return nil, errors.Wrap(ctx, reqErr, op)
|
||||
}
|
||||
if secret == nil {
|
||||
return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op))
|
||||
|
||||
@@ -15,7 +15,7 @@ var _ credential.Issuer = (*Repository)(nil)
|
||||
|
||||
// Issue issues and returns dynamic credentials from Vault for all of the
|
||||
// requests and assigns them to sessionId.
|
||||
func (r *Repository) Issue(ctx context.Context, sessionId string, requests []credential.Request) ([]credential.Dynamic, error) {
|
||||
func (r *Repository) Issue(ctx context.Context, sessionId string, requests []credential.Request, opt ...credential.Option) ([]credential.Dynamic, error) {
|
||||
const op = "vault.(Repository).Issue"
|
||||
if sessionId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "no session id")
|
||||
@@ -37,7 +37,7 @@ func (r *Repository) Issue(ctx context.Context, sessionId string, requests []cre
|
||||
var minLease time.Duration
|
||||
runJobsInterval := r.scheduler.GetRunJobsInterval()
|
||||
for _, lib := range libs {
|
||||
cred, err := lib.retrieveCredential(ctx, op, sessionId)
|
||||
cred, err := lib.retrieveCredential(ctx, op, sessionId, opt...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
|
||||
}
|
||||
grants = append(grants, roleGrant)
|
||||
|
||||
roleGrant, err = NewRoleGrant(defaultRolePublicId, "id={{account.id}};actions=read,change-password")
|
||||
roleGrant, err = NewRoleGrant(defaultRolePublicId, "id={{.Account.Id}};actions=read,change-password")
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant"))
|
||||
}
|
||||
|
||||
@@ -334,11 +334,11 @@ func Parse(scopeId, grantString string, opt ...Option) (Grant, error) {
|
||||
id := strings.TrimSuffix(strings.TrimPrefix(grant.id, "{{"), "}}")
|
||||
id = strings.TrimSpace(id)
|
||||
switch id {
|
||||
case "user.id":
|
||||
case "user.id", ".User.Id":
|
||||
if opts.withUserId != "" {
|
||||
grant.id = opts.withUserId
|
||||
}
|
||||
case "account.id":
|
||||
case "account.id", ".Account.Id":
|
||||
if opts.withAccountId != "" {
|
||||
grant.id = opts.withAccountId
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package util
|
||||
|
||||
// Pointer is just a generic function to return a pointer of whatever type is
|
||||
// given
|
||||
func Pointer[T any](input T) *T {
|
||||
ret := input
|
||||
return &ret
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package template
|
||||
|
||||
/*
|
||||
The template package provides some common functions and methods to enable
|
||||
templating Boundary data into various places. This will not necessarily be a
|
||||
one-stop-shop for all templating needs, but using this package and its Data
|
||||
struct can help ensure consistency within templates.
|
||||
*/
|
||||
@@ -0,0 +1,11 @@
|
||||
package template
|
||||
|
||||
import "strings"
|
||||
|
||||
// truncateFrom will truncate a string after the first encounter of sep; sep is
|
||||
// elided. This is a passthrough to strings.Cut with only the first return
|
||||
// value.
|
||||
func truncateFrom(str, sep string) string {
|
||||
before, _, _ := strings.Cut(str, sep)
|
||||
return before
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// Parsed contains information about a template parsed via New. Technically
|
||||
// `raw` and `funcMap` are not required to be cached here as they are part of
|
||||
// the `template.Template` object but it is useful for tests.
|
||||
type Parsed struct {
|
||||
raw string
|
||||
tmpl *template.Template
|
||||
funcMap template.FuncMap
|
||||
}
|
||||
|
||||
// New creates a Parsed struct. It requires the raw string.
|
||||
func New(ctx context.Context, raw string) (*Parsed, error) {
|
||||
const op = "util.template.New"
|
||||
|
||||
if raw == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "empty raw template")
|
||||
}
|
||||
|
||||
ret := &Parsed{
|
||||
raw: raw,
|
||||
funcMap: map[string]interface{}{
|
||||
"truncateFrom": truncateFrom,
|
||||
},
|
||||
}
|
||||
|
||||
tmpl, err := template.New("template").
|
||||
Funcs(ret.funcMap).
|
||||
Parse(ret.raw)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error parsing template"))
|
||||
}
|
||||
ret.tmpl = tmpl
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Generate based on the provided template
|
||||
func (p *Parsed) Generate(ctx context.Context, data interface{}) (string, error) {
|
||||
const op = "util.template.(Parsed).Generate"
|
||||
|
||||
switch {
|
||||
case p.tmpl == nil:
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "parsed template not initialized")
|
||||
case util.IsNil(data):
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "input data is nil")
|
||||
}
|
||||
|
||||
str := &strings.Builder{}
|
||||
if err := p.tmpl.Execute(str, data); err != nil {
|
||||
return "", errors.Wrap(ctx, err, op, errors.WithMsg("error executing template"))
|
||||
}
|
||||
|
||||
out := str.String()
|
||||
if strings.Contains(out, "<nil>") {
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "template execution resulted in nil value")
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// NOTE: Although this does a basic test of template generation, other tests
|
||||
// that are closer to where this library is used can and should also be enhanced
|
||||
// over time. For instance, TestAuthorizeSession from target_service_test.go has
|
||||
// been enhanced to use this library to check output against Vault.
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// No template
|
||||
parsed, err := New(ctx, "")
|
||||
assert.Error(err)
|
||||
assert.Nil(parsed)
|
||||
|
||||
// Bad template
|
||||
parsed, err = New(ctx, "{{ foobar")
|
||||
assert.Error(err)
|
||||
assert.Nil(parsed)
|
||||
|
||||
// Good template
|
||||
ts := "Foobar:{{ .User.Name }}"
|
||||
parsed, err = New(ctx, ts)
|
||||
assert.NoError(err)
|
||||
require.NotNil(parsed)
|
||||
assert.Equal(ts, parsed.raw)
|
||||
assert.NotNil(parsed.tmpl)
|
||||
assert.Len(parsed.funcMap, 1)
|
||||
|
||||
// Test out errors on the parsed value
|
||||
|
||||
// Nil template
|
||||
oldTmpl := parsed.tmpl
|
||||
parsed.tmpl = nil
|
||||
out, err := parsed.Generate(ctx, Data{})
|
||||
assert.Error(err)
|
||||
assert.Empty(out)
|
||||
parsed.tmpl = oldTmpl
|
||||
|
||||
// Nil
|
||||
out, err = parsed.Generate(ctx, nil)
|
||||
assert.Error(err)
|
||||
assert.Empty(out)
|
||||
|
||||
// Nil pointer
|
||||
var nilData *Data
|
||||
out, err = parsed.Generate(ctx, nilData)
|
||||
assert.Error(err)
|
||||
assert.Empty(out)
|
||||
|
||||
// Nil internal data
|
||||
_, err = parsed.Generate(ctx, Data{})
|
||||
assert.Error(err)
|
||||
|
||||
// Good data
|
||||
out, err = parsed.Generate(ctx, Data{User: User{Name: util.Pointer("name")}})
|
||||
require.NoError(err)
|
||||
assert.Equal("Foobar:name", out)
|
||||
}
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
ctx := context.Background()
|
||||
data := Data{
|
||||
User: User{
|
||||
Id: util.Pointer("userId"),
|
||||
Name: util.Pointer("userName"),
|
||||
FullName: util.Pointer("userFullName"),
|
||||
Email: util.Pointer("user@email.com"),
|
||||
},
|
||||
Account: Account{
|
||||
Id: util.Pointer("accountId"),
|
||||
Name: util.Pointer("accountName"),
|
||||
LoginName: util.Pointer("accountLoginName"),
|
||||
Subject: util.Pointer("accountSubject"),
|
||||
Email: util.Pointer("account@email.com"),
|
||||
},
|
||||
}
|
||||
raw := strings.TrimSpace(`
|
||||
{{ .User.Id }}
|
||||
{{ .User.Name }}
|
||||
{{ .User.FullName }}
|
||||
{{ .User.Email }}
|
||||
{{ truncateFrom .User.Email "@" }}
|
||||
{{ .Account.Id }}
|
||||
{{ .Account.Name }}
|
||||
{{ .Account.LoginName }}
|
||||
{{ .Account.Subject }}
|
||||
{{ .Account.Email }}
|
||||
{{ truncateFrom .Account.Email "@" }}
|
||||
`)
|
||||
|
||||
parsed, err := New(ctx, raw)
|
||||
require.NoError(err)
|
||||
require.NotNil(parsed)
|
||||
|
||||
// Ensure we error with required data
|
||||
_, err = parsed.Generate(ctx, Data{})
|
||||
require.Error(err)
|
||||
|
||||
// Do again with non-empty data
|
||||
out, err := parsed.Generate(ctx, data)
|
||||
require.NoError(err)
|
||||
|
||||
exp := strings.TrimSpace(`
|
||||
userId
|
||||
userName
|
||||
userFullName
|
||||
user@email.com
|
||||
user
|
||||
accountId
|
||||
accountName
|
||||
accountLoginName
|
||||
accountSubject
|
||||
account@email.com
|
||||
account
|
||||
`)
|
||||
|
||||
assert.Equal(exp, out)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package template
|
||||
|
||||
// Data is the top-level struct containing the various domains of templated
|
||||
// data. The Generate function can take in any data but this provides a shared
|
||||
// structure for general use.
|
||||
type Data struct {
|
||||
User User
|
||||
Account Account
|
||||
}
|
||||
|
||||
// User contains user information. FullName and Email are not always populated
|
||||
// or may be different than the values in the Account struct; these are set on
|
||||
// the user by an account from the primary auth method in a scope. It is
|
||||
// possible for a user to not have an account from that auth method (in which
|
||||
// case it will not be populated), or for the token they have used for the
|
||||
// request to be from a different auth method, in which case it may not match
|
||||
// what's in the Account struct.
|
||||
type User struct {
|
||||
Id *string
|
||||
Name *string
|
||||
FullName *string
|
||||
Email *string
|
||||
}
|
||||
|
||||
// Account contains account information. Not all fields will always be
|
||||
// populated; it depends on the type of account.
|
||||
type Account struct {
|
||||
Id *string
|
||||
Name *string
|
||||
LoginName *string
|
||||
Subject *string
|
||||
Email *string
|
||||
}
|
||||
@@ -112,11 +112,19 @@ Such a grant is essentially a full administrator grant for a scope.
|
||||
A few template possibilities exist, which will at grant evaluation time
|
||||
substitute the given value into the ID field of the grant string:
|
||||
|
||||
- `{{account.id}}`: The substituted value is the account ID associated with the
|
||||
- `{{.Account.Id}}`: The substituted value is the account ID associated with the
|
||||
token used to perform the action. As an example,
|
||||
`id={{account.id}};actions=read,change-password"` is one of Boundary's default
|
||||
grants to allow users that have authenticated with the Password auth method to
|
||||
change their own password.
|
||||
`id={{.Account.Id}};actions=read,change-password"` is one of Boundary's
|
||||
default grants to allow users that have authenticated with the Password auth
|
||||
method to change their own password.
|
||||
- **NOTE**: Prior to Boundary 0.11.1, `{{account.id}}` must be used instead.
|
||||
Boundary 0.11.1+ changes this for consistency with other places within
|
||||
Boundary that are gaining templating support, but supports both formats for
|
||||
backwards compatibility.
|
||||
|
||||
- `{{user.id}}`: The substituted value is the user ID associated with the token
|
||||
- `{{.User.Id}}`: The substituted value is the user ID associated with the token
|
||||
used to perform the action.
|
||||
- **NOTE**: Prior to Boundary 0.11.1, `{{user.id}}` must be used instead.
|
||||
Boundary 0.11.1+ changes this for consistency with other places within
|
||||
Boundary that are gaining templating support, but supports both formats for
|
||||
backwards compatibility.
|
||||
@@ -323,7 +323,7 @@ $ boundary roles add-grants -id <global_anon_listing_id> \
|
||||
-recovery-config /tmp/recovery.hcl \
|
||||
-grant 'id=*;type=auth-method;actions=list,authenticate' \
|
||||
-grant 'id=*;type=scope;actions=list,no-op' \
|
||||
-grant 'id={{account.id}};actions=read,change-password'
|
||||
-grant 'id={{.Account.Id}};actions=read,change-password'
|
||||
|
||||
$ boundary roles add-principals -id <global_anon_listing_id> \
|
||||
-recovery-config /tmp/recovery.hcl \
|
||||
@@ -340,7 +340,7 @@ resource "boundary_role" "global_anon_listing" {
|
||||
grant_strings = [
|
||||
"id=*;type=auth-method;actions=list,authenticate",
|
||||
"id=*;type=scope;actions=list,no-op",
|
||||
"id={{account.id}};actions=read,change-password"
|
||||
"id={{.Account.Id}};actions=read,change-password"
|
||||
]
|
||||
principal_ids = ["u_anon"]
|
||||
}
|
||||
@@ -367,7 +367,7 @@ $ boundary roles add-grants -id <org_anon_listing_id> \
|
||||
-recovery-config /tmp/recovery.hcl \
|
||||
-grant 'id=*;type=auth-method;actions=list,authenticate' \
|
||||
-grant 'type=scope;actions=list' \
|
||||
-grant 'id={{account.id}};actions=read,change-password'
|
||||
-grant 'id={{.Account.Id}};actions=read,change-password'
|
||||
|
||||
$ boundary roles add-principals -id <org_anon_listing_id> \
|
||||
-recovery-config /tmp/recovery.hcl \
|
||||
@@ -384,7 +384,7 @@ resource "boundary_role" "org_anon_listing" {
|
||||
grant_strings = [
|
||||
"id=*;type=auth-method;actions=list,authenticate",
|
||||
"type=scope;actions=list",
|
||||
"id={{account.id}};actions=read,change-password"
|
||||
"id={{.Account.Id}};actions=read,change-password"
|
||||
]
|
||||
principal_ids = ["u_anon"]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user