open-vault/vault/token_store.go

1450 lines
43 KiB
Go

package vault
import (
"encoding/json"
"fmt"
"regexp"
"sort"
"strings"
"time"
"github.com/armon/go-metrics"
"github.com/fatih/structs"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/salt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
"github.com/mitchellh/mapstructure"
)
const (
// lookupPrefix is the prefix used to store tokens for their
// primary ID based index
lookupPrefix = "id/"
// accessorPrefix is the prefix used to store the index from
// Accessor to Token ID
accessorPrefix = "accessor/"
// parentPrefix is the prefix used to store tokens for their
// secondar parent based index
parentPrefix = "parent/"
// tokenSubPath is the sub-path used for the token store
// view. This is nested under the system view.
tokenSubPath = "token/"
// rolesPrefix is the prefix used to store role information
rolesPrefix = "roles/"
)
var (
// displayNameSanitize is used to sanitize a display name given to a token.
displayNameSanitize = regexp.MustCompile("[^a-zA-Z0-9-]")
// pathSuffixSanitize is used to ensure a path suffix in a role is valid.
pathSuffixSanitize = regexp.MustCompile("\\w[\\w-.]+\\w")
)
// TokenStore is used to manage client tokens. Tokens are used for
// clients to authenticate, and each token is mapped to an applicable
// set of policy which is used for authorization.
type TokenStore struct {
*framework.Backend
view *BarrierView
salt *salt.Salt
expiration *ExpirationManager
cubbyholeBackend *CubbyholeBackend
policyLookupFunc func(string) (*Policy, error)
}
// NewTokenStore is used to construct a token store that is
// backed by the given barrier view.
func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error) {
// Create a sub-view
view := c.systemBarrierView.SubView(tokenSubPath)
// Initialize the store
t := &TokenStore{
view: view,
}
if c.policyStore != nil {
t.policyLookupFunc = c.policyStore.GetPolicy
}
// Setup the salt
salt, err := salt.NewSalt(view, &salt.Config{
HashFunc: salt.SHA1Hash,
})
if err != nil {
return nil, err
}
t.salt = salt
// Setup the framework endpoints
t.Backend = &framework.Backend{
AuthRenew: t.authRenew,
PathsSpecial: &logical.Paths{
Root: []string{
"revoke-prefix/*",
"revoke-orphan/*",
},
},
Paths: []*framework.Path{
&framework.Path{
Pattern: "roles/?$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: t.tokenStoreRoleList,
},
HelpSynopsis: tokenListRolesHelp,
HelpDescription: tokenListRolesHelp,
},
&framework.Path{
Pattern: "roles/" + framework.GenericNameRegex("role_name"),
Fields: map[string]*framework.FieldSchema{
"role_name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role",
},
"allowed_policies": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: tokenAllowedPoliciesHelp,
},
"orphan": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: tokenOrphanHelp,
},
"period": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 0,
Description: tokenPeriodHelp,
},
"path_suffix": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: tokenPathSuffixHelp + pathSuffixSanitize.String(),
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: t.tokenStoreRoleRead,
logical.CreateOperation: t.tokenStoreRoleCreateUpdate,
logical.UpdateOperation: t.tokenStoreRoleCreateUpdate,
logical.DeleteOperation: t.tokenStoreRoleDelete,
},
ExistenceCheck: t.tokenStoreRoleExistenceCheck,
HelpSynopsis: tokenPathRolesHelp,
HelpDescription: tokenPathRolesHelp,
},
&framework.Path{
Pattern: "create-orphan$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleCreateOrphan,
},
HelpSynopsis: strings.TrimSpace(tokenCreateOrphanHelp),
HelpDescription: strings.TrimSpace(tokenCreateOrphanHelp),
},
&framework.Path{
Pattern: "create/" + framework.GenericNameRegex("role_name"),
Fields: map[string]*framework.FieldSchema{
"role_name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleCreateAgainstRole,
},
HelpSynopsis: strings.TrimSpace(tokenCreateRoleHelp),
HelpDescription: strings.TrimSpace(tokenCreateRoleHelp),
},
&framework.Path{
Pattern: "create$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleCreate,
},
HelpSynopsis: strings.TrimSpace(tokenCreateHelp),
HelpDescription: strings.TrimSpace(tokenCreateHelp),
},
&framework.Path{
Pattern: "lookup" + framework.OptionalParamRegex("token"),
Fields: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token to lookup",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: t.handleLookup,
logical.UpdateOperation: t.handleLookup,
},
HelpSynopsis: strings.TrimSpace(tokenLookupHelp),
HelpDescription: strings.TrimSpace(tokenLookupHelp),
},
&framework.Path{
Pattern: "lookup-accessor" + framework.OptionalParamRegex("accessor"),
Fields: map[string]*framework.FieldSchema{
"accessor": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Accessor of the token to lookup",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleUpdateLookupAccessor,
},
HelpSynopsis: strings.TrimSpace(tokenLookupAccessorHelp),
HelpDescription: strings.TrimSpace(tokenLookupAccessorHelp),
},
&framework.Path{
Pattern: "lookup-self$",
Fields: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token to lookup",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: t.handleLookup,
},
HelpSynopsis: strings.TrimSpace(tokenLookupHelp),
HelpDescription: strings.TrimSpace(tokenLookupHelp),
},
&framework.Path{
Pattern: "revoke-accessor" + framework.OptionalParamRegex("accessor"),
Fields: map[string]*framework.FieldSchema{
"accessor": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Accessor of the token",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleUpdateRevokeAccessor,
},
HelpSynopsis: strings.TrimSpace(tokenRevokeAccessorHelp),
HelpDescription: strings.TrimSpace(tokenRevokeAccessorHelp),
},
&framework.Path{
Pattern: "revoke-self$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleRevokeSelf,
},
HelpSynopsis: strings.TrimSpace(tokenRevokeSelfHelp),
HelpDescription: strings.TrimSpace(tokenRevokeSelfHelp),
},
&framework.Path{
Pattern: "revoke" + framework.OptionalParamRegex("token"),
Fields: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token to revoke",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleRevokeTree,
},
HelpSynopsis: strings.TrimSpace(tokenRevokeHelp),
HelpDescription: strings.TrimSpace(tokenRevokeHelp),
},
&framework.Path{
Pattern: "revoke-orphan" + framework.OptionalParamRegex("token"),
Fields: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token to revoke",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleRevokeOrphan,
},
HelpSynopsis: strings.TrimSpace(tokenRevokeOrphanHelp),
HelpDescription: strings.TrimSpace(tokenRevokeOrphanHelp),
},
&framework.Path{
Pattern: "revoke-prefix" + framework.OptionalParamRegex("prefix"),
Fields: map[string]*framework.FieldSchema{
"prefix": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token source prefix to revoke",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleRevokePrefix,
},
HelpSynopsis: strings.TrimSpace(tokenRevokePrefixHelp),
HelpDescription: strings.TrimSpace(tokenRevokePrefixHelp),
},
&framework.Path{
Pattern: "renew-self$",
Fields: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token to renew",
},
"increment": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 0,
Description: "The desired increment in seconds to the token expiration",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleRenewSelf,
},
HelpSynopsis: strings.TrimSpace(tokenRenewSelfHelp),
HelpDescription: strings.TrimSpace(tokenRenewSelfHelp),
},
&framework.Path{
Pattern: "renew" + framework.OptionalParamRegex("token"),
Fields: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token to renew",
},
"increment": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 0,
Description: "The desired increment in seconds to the token expiration",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleRenew,
},
HelpSynopsis: strings.TrimSpace(tokenRenewHelp),
HelpDescription: strings.TrimSpace(tokenRenewHelp),
},
},
}
t.Backend.Setup(config)
return t, nil
}
// TokenEntry is used to represent a given token
type TokenEntry struct {
ID string // ID of this entry, generally a random UUID
Accessor string // Accessor for this token, a random UUID
Parent string // Parent token, used for revocation trees
Policies []string // Which named policies should be used
Path string // Used for audit trails, this is something like "auth/user/login"
Meta map[string]string // Used for auditing. This could include things like "source", "user", "ip"
DisplayName string // Used for operators to be able to associate with the source
NumUses int // Used to restrict the number of uses (zero is unlimited). This is to support one-time-tokens (generalized).
CreationTime int64 // Time of token creation
TTL time.Duration // Duration set when token was created
Role string // If set, the role that was used for parameters at creation time
}
// tsRoleEntry contains token store role information
type tsRoleEntry struct {
// The name of the role. Embedded so it can be used for pathing
Name string `json:"name" mapstructure:"name" structs:"name"`
// The policies that creation functions using this role can assign to a token,
// escaping or further locking down normal subset checking
AllowedPolicies []string `json:"allowed_policies" mapstructure:"allowed_policies" structs:"allowed_policies"`
// If true, tokens created using this role will be orphans
Orphan bool `json:"orphan" mapstructure:"orphan" structs:"orphan"`
// If non-zero, tokens created using this role will be able to be renewed
// forever, but will have a fixed renewal period of this value
Period time.Duration `json:"period" mapstructure:"period" structs:"period"`
// If set, a suffix will be set on the token path, making it easier to
// revoke using 'revoke-prefix'.
PathSuffix string `json:"path_suffix" mapstructure:"path_suffix" structs:"path_suffix"`
}
// SetExpirationManager is used to provide the token store with
// an expiration manager. This is used to manage prefix based revocation
// of tokens and to cleanup entries when removed from the token store.
func (ts *TokenStore) SetExpirationManager(exp *ExpirationManager) {
ts.expiration = exp
}
// SaltID is used to apply a salt and hash to an ID to make sure its not reversable
func (ts *TokenStore) SaltID(id string) string {
return ts.salt.SaltID(id)
}
// RootToken is used to generate a new token with root privileges and no parent
func (ts *TokenStore) rootToken() (*TokenEntry, error) {
te := &TokenEntry{
Policies: []string{"root"},
Path: "auth/token/root",
DisplayName: "root",
CreationTime: time.Now().Unix(),
}
if err := ts.create(te); err != nil {
return nil, err
}
return te, nil
}
// createAccessor is used to create an identifier for the token ID.
// A storage index, mapping the accessor to the token ID is also created.
func (ts *TokenStore) createAccessor(entry *TokenEntry) error {
defer metrics.MeasureSince([]string{"token", "createAccessor"}, time.Now())
// Create a random accessor
accessorUUID, err := uuid.GenerateUUID()
if err != nil {
return err
}
entry.Accessor = accessorUUID
// Create index entry, mapping the accessor to the token ID
path := accessorPrefix + ts.SaltID(entry.Accessor)
le := &logical.StorageEntry{Key: path, Value: []byte(entry.ID)}
if err := ts.view.Put(le); err != nil {
return fmt.Errorf("failed to persist accessor index entry: %v", err)
}
return nil
}
// Create is used to create a new token entry. The entry is assigned
// a newly generated ID if not provided.
func (ts *TokenStore) create(entry *TokenEntry) error {
defer metrics.MeasureSince([]string{"token", "create"}, time.Now())
// Generate an ID if necessary
if entry.ID == "" {
entryUUID, err := uuid.GenerateUUID()
if err != nil {
return err
}
entry.ID = entryUUID
}
err := ts.createAccessor(entry)
if err != nil {
return err
}
return ts.storeCommon(entry, true)
}
// Store is used to store an updated token entry without writing the
// secondary index.
func (ts *TokenStore) store(entry *TokenEntry) error {
defer metrics.MeasureSince([]string{"token", "store"}, time.Now())
return ts.storeCommon(entry, false)
}
// storeCommon handles the actual storage of an entry, possibly generating
// secondary indexes
func (ts *TokenStore) storeCommon(entry *TokenEntry, writeSecondary bool) error {
saltedId := ts.SaltID(entry.ID)
// Marshal the entry
enc, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to encode entry: %v", err)
}
if writeSecondary {
// Write the secondary index if necessary. This is done before the
// primary index because we'd rather have a dangling pointer with
// a missing primary instead of missing the parent index and potentially
// escaping the revocation chain.
if entry.Parent != "" {
// Ensure the parent exists
parent, err := ts.Lookup(entry.Parent)
if err != nil {
return fmt.Errorf("failed to lookup parent: %v", err)
}
if parent == nil {
return fmt.Errorf("parent token not found")
}
// Create the index entry
path := parentPrefix + ts.SaltID(entry.Parent) + "/" + saltedId
le := &logical.StorageEntry{Key: path}
if err := ts.view.Put(le); err != nil {
return fmt.Errorf("failed to persist entry: %v", err)
}
}
}
// Write the primary ID
path := lookupPrefix + saltedId
le := &logical.StorageEntry{Key: path, Value: enc}
if err := ts.view.Put(le); err != nil {
return fmt.Errorf("failed to persist entry: %v", err)
}
return nil
}
// UseToken is used to manage restricted use tokens and decrement
// their available uses.
func (ts *TokenStore) UseToken(te *TokenEntry) error {
// If the token is not restricted, there is nothing to do
if te.NumUses == 0 {
return nil
}
// Decrement the count
te.NumUses -= 1
// Revoke the token if there are no remaining uses.
// XXX: There is a race condition here with parallel
// requests using the same token. This would require
// some global coordination to avoid, as we must ensure
// no requests using the same restricted token are handled
// in parallel.
if te.NumUses == 0 {
return ts.Revoke(te.ID)
}
// Marshal the entry
enc, err := json.Marshal(te)
if err != nil {
return fmt.Errorf("failed to encode entry: %v", err)
}
// Write under the primary ID
saltedId := ts.SaltID(te.ID)
path := lookupPrefix + saltedId
le := &logical.StorageEntry{Key: path, Value: enc}
if err := ts.view.Put(le); err != nil {
return fmt.Errorf("failed to persist entry: %v", err)
}
return nil
}
// Lookup is used to find a token given its ID
func (ts *TokenStore) Lookup(id string) (*TokenEntry, error) {
defer metrics.MeasureSince([]string{"token", "lookup"}, time.Now())
if id == "" {
return nil, fmt.Errorf("cannot lookup blank token")
}
return ts.lookupSalted(ts.SaltID(id))
}
// lookupSlated is used to find a token given its salted ID
func (ts *TokenStore) lookupSalted(saltedId string) (*TokenEntry, error) {
// Lookup token
path := lookupPrefix + saltedId
raw, err := ts.view.Get(path)
if err != nil {
return nil, fmt.Errorf("failed to read entry: %v", err)
}
// Bail if not found
if raw == nil {
return nil, nil
}
// Unmarshal the token
entry := new(TokenEntry)
if err := json.Unmarshal(raw.Value, entry); err != nil {
return nil, fmt.Errorf("failed to decode entry: %v", err)
}
return entry, nil
}
// Revoke is used to invalidate a given token, any child tokens
// will be orphaned.
func (ts *TokenStore) Revoke(id string) error {
defer metrics.MeasureSince([]string{"token", "revoke"}, time.Now())
if id == "" {
return fmt.Errorf("cannot revoke blank token")
}
return ts.revokeSalted(ts.SaltID(id))
}
// revokeSalted is used to invalidate a given salted token,
// any child tokens will be orphaned.
func (ts *TokenStore) revokeSalted(saltedId string) error {
// Lookup the token first
entry, err := ts.lookupSalted(saltedId)
if err != nil {
return err
}
// Nuke the primary key first
path := lookupPrefix + saltedId
if ts.view.Delete(path); err != nil {
return fmt.Errorf("failed to delete entry: %v", err)
}
// Clear the secondary index if any
if entry != nil && entry.Parent != "" {
path := parentPrefix + ts.SaltID(entry.Parent) + "/" + saltedId
if ts.view.Delete(path); err != nil {
return fmt.Errorf("failed to delete entry: %v", err)
}
}
// Clear the accessor index if any
if entry != nil && entry.Accessor != "" {
path := accessorPrefix + ts.SaltID(entry.Accessor)
if ts.view.Delete(path); err != nil {
return fmt.Errorf("failed to delete entry: %v", err)
}
}
// Revoke all secrets under this token
if entry != nil {
if err := ts.expiration.RevokeByToken(entry.ID); err != nil {
return err
}
}
// Destroy the cubby space
err = ts.destroyCubbyhole(saltedId)
if err != nil {
return err
}
return nil
}
// RevokeTree is used to invalide a given token and all
// child tokens.
func (ts *TokenStore) RevokeTree(id string) error {
defer metrics.MeasureSince([]string{"token", "revoke-tree"}, time.Now())
// Verify the token is not blank
if id == "" {
return fmt.Errorf("cannot revoke blank token")
}
// Get the salted ID
saltedId := ts.SaltID(id)
// Nuke the entire tree recursively
if err := ts.revokeTreeSalted(saltedId); err != nil {
return err
}
return nil
}
// revokeTreeSalted is used to invalide a given token and all
// child tokens using a saltedID.
func (ts *TokenStore) revokeTreeSalted(saltedId string) error {
// Scan for child tokens
path := parentPrefix + saltedId + "/"
children, err := ts.view.List(path)
if err != nil {
return fmt.Errorf("failed to scan for children: %v", err)
}
// Recursively nuke the children. The subtle nuance here is that
// we don't have the acutal ID of the child, but we have the salted
// value. Turns out, this is good enough!
for _, child := range children {
if err := ts.revokeTreeSalted(child); err != nil {
return err
}
}
// Revoke this entry
if err := ts.revokeSalted(saltedId); err != nil {
return fmt.Errorf("failed to revoke entry: %v", err)
}
return nil
}
// handleCreateAgainstRole handles the auth/token/create path for a role
func (ts *TokenStore) handleCreateAgainstRole(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("role_name").(string)
roleEntry, err := ts.tokenStoreRole(name)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("unknown role %s", name)), nil
}
return ts.handleCreateCommon(req, d, false, roleEntry)
}
func (ts *TokenStore) lookupByAccessor(accessor string) (string, error) {
entry, err := ts.view.Get(accessorPrefix + ts.SaltID(accessor))
if err != nil {
return "", fmt.Errorf("failed to read index using accessor: %s", err)
}
if entry == nil {
return "", &StatusBadRequest{Err: "invalid accessor"}
}
return string(entry.Value), nil
}
// handleUpdateLookupAccessor handles the auth/token/lookup-accessor path for returning
// the properties of the token associated with the accessor
func (ts *TokenStore) handleUpdateLookupAccessor(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
accessor := data.Get("accessor").(string)
if accessor == "" {
return nil, &StatusBadRequest{Err: "missing accessor"}
}
tokenID, err := ts.lookupByAccessor(accessor)
if err != nil {
return nil, err
}
// Prepare the field data required for a lookup call
d := &framework.FieldData{
Raw: map[string]interface{}{
"token": tokenID,
},
Schema: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token to lookup",
},
},
}
resp, err := ts.handleLookup(req, d)
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("failed to lookup the token")
}
if resp.IsError() {
return resp, nil
}
// Remove the token ID from the response
if resp.Data != nil {
resp.Data["id"] = ""
}
return resp, nil
}
// handleUpdateRevokeAccessor handles the auth/token/revoke-accessor path for revoking
// the token associated with the accessor
func (ts *TokenStore) handleUpdateRevokeAccessor(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
accessor := data.Get("accessor").(string)
if accessor == "" {
return nil, &StatusBadRequest{Err: "missing accessor"}
}
tokenID, err := ts.lookupByAccessor(accessor)
if err != nil {
return nil, err
}
// Revoke the token and its children
if err := ts.RevokeTree(tokenID); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil
}
// handleCreate handles the auth/token/create path for creation of new orphan
// tokens
func (ts *TokenStore) handleCreateOrphan(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return ts.handleCreateCommon(req, d, true, nil)
}
// handleCreate handles the auth/token/create path for creation of new non-orphan
// tokens
func (ts *TokenStore) handleCreate(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return ts.handleCreateCommon(req, d, false, nil)
}
// handleCreateCommon handles the auth/token/create path for creation of new tokens
func (ts *TokenStore) handleCreateCommon(
req *logical.Request, d *framework.FieldData, orphan bool, role *tsRoleEntry) (*logical.Response, error) {
// Read the parent policy
parent, err := ts.Lookup(req.ClientToken)
if err != nil || parent == nil {
return logical.ErrorResponse("parent token lookup failed"), logical.ErrInvalidRequest
}
// A token with a restricted number of uses cannot create a new token
// otherwise it could escape the restriction count.
if parent.NumUses > 0 {
return logical.ErrorResponse("restricted use token cannot generate child tokens"),
logical.ErrInvalidRequest
}
// Check if the client token has sudo/root privileges for the requested path
isSudo := ts.System().SudoPrivilege(req.MountPoint+req.Path, req.ClientToken)
// Read and parse the fields
var data struct {
ID string
Policies []string
Metadata map[string]string `mapstructure:"meta"`
NoParent bool `mapstructure:"no_parent"`
NoDefaultPolicy bool `mapstructure:"no_default_policy"`
Lease string
TTL string
DisplayName string `mapstructure:"display_name"`
NumUses int `mapstructure:"num_uses"`
}
if err := mapstructure.WeakDecode(req.Data, &data); err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Error decoding request: %s", err)), logical.ErrInvalidRequest
}
// Verify the number of uses is positive
if data.NumUses < 0 {
return logical.ErrorResponse("number of uses cannot be negative"),
logical.ErrInvalidRequest
}
// Setup the token entry
te := TokenEntry{
Parent: req.ClientToken,
Path: "auth/token/create",
Meta: data.Metadata,
DisplayName: "token",
NumUses: data.NumUses,
CreationTime: time.Now().Unix(),
}
// If the role is not nil, we add the role name as part of the token's
// path. This makes it much easier to later revoke tokens that were issued
// by a role (using revoke-prefix). Users can further specify a PathSuffix
// in the role; that way they can use something like "v1", "v2" to indicate
// role revisions, and revoke only tokens issued with a previous revision.
if role != nil {
te.Role = role.Name
te.Path = fmt.Sprintf("%s/%s", te.Path, role.Name)
if role.PathSuffix != "" {
te.Path = fmt.Sprintf("%s/%s", te.Path, role.PathSuffix)
}
}
// Attach the given display name if any
if data.DisplayName != "" {
full := "token-" + data.DisplayName
full = displayNameSanitize.ReplaceAllString(full, "-")
full = strings.TrimSuffix(full, "-")
te.DisplayName = full
}
// Allow specifying the ID of the token if the client has root or sudo privileges
if data.ID != "" {
if !isSudo {
return logical.ErrorResponse("root or sudo privileges required to specify token id"),
logical.ErrInvalidRequest
}
te.ID = data.ID
}
switch {
// If we have a role, we don't even consider parent policies; the role
// allowed policies trumps all
case role != nil:
if len(data.Policies) == 0 {
data.Policies = role.AllowedPolicies
} else {
if !strListSubset(role.AllowedPolicies, data.Policies) {
return logical.ErrorResponse("token policies must be subset of the role's allowed policies"), logical.ErrInvalidRequest
}
}
case len(data.Policies) == 0:
data.Policies = parent.Policies
// When a role is not in use, only permit policies to be a subset unless
// the client has root or sudo privileges
case !isSudo && !strListSubset(parent.Policies, data.Policies):
return logical.ErrorResponse("child policies must be subset of parent"), logical.ErrInvalidRequest
}
// Use a map to filter out/prevent duplicates
policyMap := map[string]bool{}
for _, policy := range data.Policies {
if policy == "" {
// Don't allow a policy with no name, even though it is a valid
// slice member
continue
}
policyMap[policy] = true
}
if !policyMap["root"] &&
!data.NoDefaultPolicy {
policyMap["default"] = true
}
for k, _ := range policyMap {
te.Policies = append(te.Policies, k)
}
sort.Strings(te.Policies)
switch {
case role != nil:
if role.Orphan {
te.Parent = ""
}
case data.NoParent:
// Only allow an orphan token if the client has sudo policy
if !isSudo {
return logical.ErrorResponse("root or sudo privileges required to create orphan token"),
logical.ErrInvalidRequest
}
te.Parent = ""
default:
// This comes from create-orphan, which can be properly ACLd
if orphan {
te.Parent = ""
}
}
if role != nil && role.Period > 0 {
te.TTL = role.Period
} else {
// Parse the TTL/lease if any
if data.TTL != "" {
dur, err := time.ParseDuration(data.TTL)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
if dur < 0 {
return logical.ErrorResponse("ttl must be positive"), logical.ErrInvalidRequest
}
te.TTL = dur
} else if data.Lease != "" {
dur, err := time.ParseDuration(data.Lease)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
if dur < 0 {
return logical.ErrorResponse("lease must be positive"), logical.ErrInvalidRequest
}
te.TTL = dur
}
sysView := ts.System()
// Set the default lease if non-provided, root tokens are exempt
if te.TTL == 0 && !strListContains(te.Policies, "root") {
te.TTL = sysView.DefaultLeaseTTL()
}
// Limit the lease duration
if te.TTL > sysView.MaxLeaseTTL() {
te.TTL = sysView.MaxLeaseTTL()
}
}
// Create the token
if err := ts.create(&te); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
// Generate the response
resp := &logical.Response{
Auth: &logical.Auth{
DisplayName: te.DisplayName,
Policies: te.Policies,
Metadata: te.Meta,
LeaseOptions: logical.LeaseOptions{
TTL: te.TTL,
Renewable: true,
},
ClientToken: te.ID,
Accessor: te.Accessor,
},
}
if ts.policyLookupFunc != nil {
for _, p := range te.Policies {
policy, err := ts.policyLookupFunc(p)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("could not look up policy %s", p)), nil
}
if policy == nil {
resp.AddWarning(fmt.Sprintf("policy \"%s\" does not exist", p))
}
}
}
return resp, nil
}
// handleRevokeSelf handles the auth/token/revoke-self path for revocation of tokens
// in a way that revokes all child tokens. Normally, using sys/revoke/leaseID will revoke
// the token and all children anyways, but that is only available when there is a lease.
func (ts *TokenStore) handleRevokeSelf(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Revoke the token and its children
if err := ts.RevokeTree(req.ClientToken); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil
}
// handleRevokeTree handles the auth/token/revoke/id path for revocation of tokens
// in a way that revokes all child tokens. Normally, using sys/revoke/leaseID will revoke
// the token and all children anyways, but that is only available when there is a lease.
func (ts *TokenStore) handleRevokeTree(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
id := data.Get("token").(string)
if id == "" {
return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest
}
// Revoke the token and its children
if err := ts.RevokeTree(id); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil
}
// handleRevokeOrphan handles the auth/token/revoke-orphan/id path for revocation of tokens
// in a way that leaves child tokens orphaned. Normally, using sys/revoke/leaseID will revoke
// the token and all children.
func (ts *TokenStore) handleRevokeOrphan(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Parse the id
id := data.Get("token").(string)
if id == "" {
return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest
}
parent, err := ts.Lookup(req.ClientToken)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("parent token lookup failed: %s", err.Error())), logical.ErrInvalidRequest
}
if parent == nil {
return logical.ErrorResponse("parent token lookup failed"), logical.ErrInvalidRequest
}
// Check if the client token has sudo/root privileges for the requested path
isSudo := ts.System().SudoPrivilege(req.MountPoint+req.Path, req.ClientToken)
if !isSudo {
return logical.ErrorResponse("root or sudo privileges required to revoke and orphan"),
logical.ErrInvalidRequest
}
// Revoke and orphan
if err := ts.Revoke(id); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil
}
// handleRevokePrefix handles the auth/token/revoke-prefix/path for revocation of tokens
// generated by a given path.
func (ts *TokenStore) handleRevokePrefix(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Parse the prefix
prefix := data.Get("prefix").(string)
if prefix == "" {
return logical.ErrorResponse("missing source prefix"), logical.ErrInvalidRequest
}
// Revoke using the prefix
if err := ts.expiration.RevokePrefix(prefix); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil
}
// handleLookup handles the auth/token/lookup/id path for querying information about
// a particular token. This can be used to see which policies are applicable.
func (ts *TokenStore) handleLookup(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
id := data.Get("token").(string)
if id == "" {
id = req.ClientToken
}
if id == "" {
return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest
}
// Lookup the token
out, err := ts.Lookup(id)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
if out == nil {
return logical.ErrorResponse("bad token"), logical.ErrPermissionDenied
}
// Generate a response. We purposely omit the parent reference otherwise
// you could escalate your privileges.
resp := &logical.Response{
Data: map[string]interface{}{
"id": out.ID,
"accessor": out.Accessor,
"policies": out.Policies,
"path": out.Path,
"meta": out.Meta,
"display_name": out.DisplayName,
"num_uses": out.NumUses,
"orphan": false,
"creation_time": int64(out.CreationTime),
"creation_ttl": int64(out.TTL.Seconds()),
"ttl": int64(0),
"role": out.Role,
},
}
if out.Parent == "" {
resp.Data["orphan"] = true
}
// Fetch the last renewal time
leaseTimes, err := ts.expiration.FetchLeaseTimesByToken(out.Path, out.ID)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
if leaseTimes != nil {
if !leaseTimes.LastRenewalTime.IsZero() {
resp.Data["last_renewal_time"] = leaseTimes.LastRenewalTime.Unix()
}
if !leaseTimes.ExpireTime.IsZero() {
resp.Data["ttl"] = int64(leaseTimes.ExpireTime.Sub(time.Now().Round(time.Second)).Seconds())
}
}
return resp, nil
}
func (ts *TokenStore) handleRenewSelf(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
data.Raw["token"] = req.ClientToken
return ts.handleRenew(req, data)
}
// handleRenew handles the auth/token/renew/id path for renewal of tokens.
// This is used to prevent token expiration and revocation.
func (ts *TokenStore) handleRenew(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
id := data.Get("token").(string)
if id == "" {
return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest
}
incrementRaw := data.Get("increment").(int)
// Convert the increment
increment := time.Duration(incrementRaw) * time.Second
// Lookup the token
te, err := ts.Lookup(id)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
// Verify the token exists
if te == nil {
return logical.ErrorResponse("token not found"), logical.ErrInvalidRequest
}
// Renew the token and its children
return ts.expiration.RenewToken(req, te.Path, te.ID, increment)
}
func (ts *TokenStore) destroyCubbyhole(saltedID string) error {
if ts.cubbyholeBackend == nil {
// Should only ever happen in testing
return nil
}
return ts.cubbyholeBackend.revoke(salt.SaltID(ts.cubbyholeBackend.saltUUID, saltedID, salt.SHA1Hash))
}
func (ts *TokenStore) authRenew(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
if req.Auth == nil {
return nil, fmt.Errorf("request auth is nil")
}
f := framework.LeaseExtend(req.Auth.Increment, 0, ts.System())
te, err := ts.Lookup(req.Auth.ClientToken)
if err != nil {
return nil, fmt.Errorf("error looking up token: %s", err)
}
if te == nil {
return nil, fmt.Errorf("no token entry found during lookup")
}
// No role? Use normal LeaseExtend semantics
if te.Role == "" {
return f(req, d)
}
role, err := ts.tokenStoreRole(te.Role)
if err != nil {
return nil, fmt.Errorf("error looking up role %s: %s", te.Role, err)
}
if role == nil {
return logical.ErrorResponse(fmt.Sprintf("original token role (%s) could not be found, not renewing", te.Role)), nil
}
// If role.Period is not zero, this is a periodic token. The TTL for a
// periodic token is always the same (the role's period value). It is not
// subject to normal maximum TTL checks that would come from calling
// LeaseExtend, so we fast path it.
if role.Period != 0 {
req.Auth.TTL = role.Period
return &logical.Response{Auth: req.Auth}, nil
}
return f(req, d)
}
func (ts *TokenStore) tokenStoreRole(name string) (*tsRoleEntry, error) {
entry, err := ts.view.Get(fmt.Sprintf("%s%s", rolesPrefix, name))
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result tsRoleEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (ts *TokenStore) tokenStoreRoleList(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
entries, err := ts.view.List(rolesPrefix)
if err != nil {
return nil, err
}
ret := make([]string, len(entries))
for i, entry := range entries {
ret[i] = strings.TrimPrefix(entry, rolesPrefix)
}
return logical.ListResponse(ret), nil
}
func (ts *TokenStore) tokenStoreRoleDelete(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
err := ts.view.Delete(fmt.Sprintf("%s%s", rolesPrefix, data.Get("role_name").(string)))
if err != nil {
return nil, err
}
return nil, nil
}
func (ts *TokenStore) tokenStoreRoleRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
role, err := ts.tokenStoreRole(data.Get("role_name").(string))
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
resp := &logical.Response{
Data: structs.New(role).Map(),
}
return resp, nil
}
func (ts *TokenStore) tokenStoreRoleExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
name := data.Get("role_name").(string)
if name == "" {
return false, fmt.Errorf("role name cannot be empty")
}
role, err := ts.tokenStoreRole(name)
if err != nil {
return false, err
}
return role != nil, nil
}
func (ts *TokenStore) tokenStoreRoleCreateUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("role_name").(string)
if name == "" {
return logical.ErrorResponse("role name cannot be empty"), nil
}
entry, err := ts.tokenStoreRole(name)
if err != nil {
return nil, err
}
// Due to the existence check, entry will only be nil if it's a create
// operation, so just create a new one
if entry == nil {
entry = &tsRoleEntry{
Name: name,
}
}
// In this series of blocks, if we do not find a user-provided value and
// it's a creation operation, we call data.Get to get the appropriate
// default
orphanInt, ok := data.GetOk("orphan")
if ok {
entry.Orphan = orphanInt.(bool)
} else if req.Operation == logical.CreateOperation {
entry.Orphan = data.Get("orphan").(bool)
}
periodInt, ok := data.GetOk("period")
if ok {
entry.Period = time.Second * time.Duration(periodInt.(int))
} else if req.Operation == logical.CreateOperation {
entry.Period = time.Second * time.Duration(data.Get("period").(int))
}
pathSuffixInt, ok := data.GetOk("path_suffix")
if ok {
pathSuffix := pathSuffixInt.(string)
if pathSuffix != "" {
matched := pathSuffixSanitize.MatchString(pathSuffix)
if !matched {
return logical.ErrorResponse(fmt.Sprintf("given role path suffix contains invalid characters; must match %s", pathSuffixSanitize.String())), nil
}
entry.PathSuffix = pathSuffix
}
} else if req.Operation == logical.CreateOperation {
entry.PathSuffix = data.Get("path_suffix").(string)
}
allowedPoliciesInt, ok := data.GetOk("allowed_policies")
if ok {
allowedPolicies := allowedPoliciesInt.(string)
if allowedPolicies != "" {
entry.AllowedPolicies = strings.Split(allowedPolicies, ",")
}
} else if req.Operation == logical.CreateOperation {
entry.AllowedPolicies = strings.Split(data.Get("allowed_policies").(string), ",")
}
// Store it
jsonEntry, err := logical.StorageEntryJSON(fmt.Sprintf("%s%s", rolesPrefix, name), entry)
if err != nil {
return nil, err
}
if err := ts.view.Put(jsonEntry); err != nil {
return nil, err
}
return nil, nil
}
const (
tokenBackendHelp = `The token credential backend is always enabled and builtin to Vault.
Client tokens are used to identify a client and to allow Vault to associate policies and ACLs
which are enforced on every request. This backend also allows for generating sub-tokens as well
as revocation of tokens. The tokens are renewable if associated with a lease.`
tokenCreateHelp = `The token create path is used to create new tokens.`
tokenCreateOrphanHelp = `The token create path is used to create new orphan tokens.`
tokenCreateRoleHelp = `This token create path is used to create new tokens adhering to the given role.`
tokenListRolesHelp = `This endpoint lists configured roles.`
tokenLookupAccessorHelp = `This endpoint will lookup a token associated with the given accessor and its properties. Response will not contain the token ID.`
tokenLookupHelp = `This endpoint will lookup a token and its properties.`
tokenPathRolesHelp = `This endpoint allows creating, reading, and deleting roles.`
tokenRevokeAccessorHelp = `This endpoint will delete the token associated with the accessor and all of its child tokens.`
tokenRevokeHelp = `This endpoint will delete the given token and all of its child tokens.`
tokenRevokeSelfHelp = `This endpoint will delete the token used to call it and all of its child tokens.`
tokenRevokeOrphanHelp = `This endpoint will delete the token and orphan its child tokens.`
tokenRevokePrefixHelp = `This endpoint will delete all tokens generated under a prefix with their child tokens.`
tokenRenewHelp = `This endpoint will renew the given token and prevent expiration.`
tokenRenewSelfHelp = `This endpoint will renew the token used to call it and prevent expiration.`
tokenAllowedPoliciesHelp = `If set, tokens created via this role
can be created with any subset of this list,
rather than the normal semantics of a subset
of the client token's policies. This
parameter should be sent as a comma-delimited
string.`
tokenOrphanHelp = `If true, tokens created via this role
will be orphan tokens (have no parent)`
tokenPeriodHelp = `If set, tokens created via this role
will have no max lifetime; instead, their
renewal period will be fixed to this value.
This takes an integer number of seconds,
or a string duration (e.g. "24h").`
tokenPathSuffixHelp = `If set, tokens created via this role
will contain the given suffix as a part of
their path. This can be used to assist use
of the 'revoke-prefix' endpoint later on.
The given suffix must match the regular
expression `
)