2015-03-18 20:19:19 +00:00
|
|
|
package vault
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha1"
|
|
|
|
"encoding/hex"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2015-04-15 21:24:07 +00:00
|
|
|
"regexp"
|
2015-03-24 22:10:46 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2015-03-18 20:19:19 +00:00
|
|
|
|
2015-04-08 23:43:17 +00:00
|
|
|
"github.com/armon/go-metrics"
|
2015-06-30 19:38:32 +00:00
|
|
|
"github.com/hashicorp/vault/helper/uuid"
|
2015-03-18 20:19:19 +00:00
|
|
|
"github.com/hashicorp/vault/logical"
|
2015-03-31 19:48:19 +00:00
|
|
|
"github.com/hashicorp/vault/logical/framework"
|
2015-04-07 21:16:35 +00:00
|
|
|
"github.com/mitchellh/mapstructure"
|
2015-03-18 20:19:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// lookupPrefix is the prefix used to store tokens for their
|
|
|
|
// primary ID based index
|
|
|
|
lookupPrefix = "id/"
|
|
|
|
|
|
|
|
// parentPrefix is the prefix used to store tokens for their
|
|
|
|
// secondar parent based index
|
|
|
|
parentPrefix = "parent/"
|
|
|
|
|
|
|
|
// tokenSaltLocation is the path in the view we store our key salt.
|
|
|
|
// This is used to ensure the paths we write out are obfuscated so
|
|
|
|
// that token names cannot be guessed as that would compromise their
|
|
|
|
// use.
|
|
|
|
tokenSaltLocation = "salt"
|
2015-03-18 21:00:42 +00:00
|
|
|
|
|
|
|
// tokenSubPath is the sub-path used for the token store
|
|
|
|
// view. This is nested under the system view.
|
|
|
|
tokenSubPath = "token/"
|
2015-03-18 20:19:19 +00:00
|
|
|
)
|
|
|
|
|
2015-04-15 21:24:07 +00:00
|
|
|
var (
|
|
|
|
// displayNameSanitize is used to sanitize a display name given to a token.
|
|
|
|
displayNameSanitize = regexp.MustCompile("[^a-zA-Z0-9-]")
|
|
|
|
)
|
|
|
|
|
2015-03-18 20:19:19 +00:00
|
|
|
// 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 {
|
2015-03-31 19:48:19 +00:00
|
|
|
*framework.Backend
|
|
|
|
|
2015-03-18 20:19:19 +00:00
|
|
|
view *BarrierView
|
|
|
|
salt string
|
2015-04-03 18:40:08 +00:00
|
|
|
|
|
|
|
expiration *ExpirationManager
|
2015-03-18 20:19:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewTokenStore is used to construct a token store that is
|
|
|
|
// backed by the given barrier view.
|
2015-03-19 02:11:52 +00:00
|
|
|
func NewTokenStore(c *Core) (*TokenStore, error) {
|
|
|
|
// Create a sub-view
|
|
|
|
view := c.systemView.SubView(tokenSubPath)
|
|
|
|
|
2015-03-18 20:19:19 +00:00
|
|
|
// Initialize the store
|
|
|
|
t := &TokenStore{
|
|
|
|
view: view,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Look for the salt
|
|
|
|
raw, err := view.Get(tokenSaltLocation)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read salt: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Restore the salt if it exists
|
|
|
|
if raw != nil {
|
|
|
|
t.salt = string(raw.Value)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a new salt if necessary
|
|
|
|
if t.salt == "" {
|
2015-06-30 19:38:32 +00:00
|
|
|
t.salt = uuid.GenerateUUID()
|
2015-03-18 20:19:19 +00:00
|
|
|
raw = &logical.StorageEntry{Key: tokenSaltLocation, Value: []byte(t.salt)}
|
|
|
|
if err := view.Put(raw); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to persist salt: %v", err)
|
|
|
|
}
|
|
|
|
}
|
2015-03-31 19:48:19 +00:00
|
|
|
|
|
|
|
// Setup the framework endpoints
|
|
|
|
t.Backend = &framework.Backend{
|
2015-06-17 21:28:13 +00:00
|
|
|
// Allow a token lease to be extended indefinitely, but each time for only
|
|
|
|
// as much as the original lease allowed for. If the lease has a 1 hour expiration,
|
|
|
|
// it can only be extended up to another hour each time this means.
|
|
|
|
AuthRenew: framework.LeaseExtend(0, 0, true),
|
2015-04-11 23:28:16 +00:00
|
|
|
|
2015-04-03 18:40:08 +00:00
|
|
|
PathsSpecial: &logical.Paths{
|
|
|
|
Root: []string{
|
|
|
|
"revoke-prefix/*",
|
|
|
|
},
|
2015-04-09 05:09:08 +00:00
|
|
|
|
|
|
|
Unauthenticated: []string{
|
|
|
|
"lookup-self",
|
|
|
|
},
|
2015-04-03 18:40:08 +00:00
|
|
|
},
|
|
|
|
|
2015-03-31 19:48:19 +00:00
|
|
|
Paths: []*framework.Path{
|
|
|
|
&framework.Path{
|
|
|
|
Pattern: "create$",
|
|
|
|
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
|
|
logical.WriteOperation: t.handleCreate,
|
|
|
|
},
|
|
|
|
|
|
|
|
HelpSynopsis: strings.TrimSpace(tokenCreateHelp),
|
|
|
|
HelpDescription: strings.TrimSpace(tokenCreateHelp),
|
|
|
|
},
|
|
|
|
|
|
|
|
&framework.Path{
|
2015-03-31 19:51:00 +00:00
|
|
|
Pattern: "lookup/(?P<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,
|
|
|
|
},
|
|
|
|
|
|
|
|
HelpSynopsis: strings.TrimSpace(tokenLookupHelp),
|
|
|
|
HelpDescription: strings.TrimSpace(tokenLookupHelp),
|
|
|
|
},
|
|
|
|
|
|
|
|
&framework.Path{
|
|
|
|
Pattern: "lookup-self$",
|
2015-03-31 19:48:19 +00:00
|
|
|
|
|
|
|
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/(?P<token>.+)",
|
|
|
|
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
|
|
"token": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "Token to revoke",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
|
|
logical.WriteOperation: t.handleRevokeTree,
|
|
|
|
},
|
|
|
|
|
|
|
|
HelpSynopsis: strings.TrimSpace(tokenRevokeHelp),
|
|
|
|
HelpDescription: strings.TrimSpace(tokenRevokeHelp),
|
|
|
|
},
|
2015-04-03 19:11:49 +00:00
|
|
|
|
2015-03-31 19:48:19 +00:00
|
|
|
&framework.Path{
|
|
|
|
Pattern: "revoke-orphan/(?P<token>.+)",
|
|
|
|
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
|
|
"token": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "Token to revoke",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
|
|
logical.WriteOperation: t.handleRevokeOrphan,
|
|
|
|
},
|
|
|
|
|
|
|
|
HelpSynopsis: strings.TrimSpace(tokenRevokeOrphanHelp),
|
|
|
|
HelpDescription: strings.TrimSpace(tokenRevokeOrphanHelp),
|
|
|
|
},
|
2015-04-03 18:40:08 +00:00
|
|
|
|
|
|
|
&framework.Path{
|
|
|
|
Pattern: "revoke-prefix/(?P<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.WriteOperation: t.handleRevokePrefix,
|
|
|
|
},
|
|
|
|
|
|
|
|
HelpSynopsis: strings.TrimSpace(tokenRevokePrefixHelp),
|
|
|
|
HelpDescription: strings.TrimSpace(tokenRevokePrefixHelp),
|
|
|
|
},
|
2015-04-03 19:11:49 +00:00
|
|
|
|
|
|
|
&framework.Path{
|
|
|
|
Pattern: "renew/(?P<token>.+)",
|
|
|
|
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
|
|
"token": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "Token to renew",
|
|
|
|
},
|
2015-04-09 21:23:37 +00:00
|
|
|
"increment": &framework.FieldSchema{
|
2015-06-17 22:58:20 +00:00
|
|
|
Type: framework.TypeDurationSecond,
|
2015-04-09 21:23:37 +00:00
|
|
|
Description: "The desired increment in seconds to the token expiration",
|
|
|
|
},
|
2015-04-03 19:11:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
|
|
logical.WriteOperation: t.handleRenew,
|
|
|
|
},
|
|
|
|
|
|
|
|
HelpSynopsis: strings.TrimSpace(tokenRenewHelp),
|
|
|
|
HelpDescription: strings.TrimSpace(tokenRenewHelp),
|
|
|
|
},
|
2015-03-31 19:48:19 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2015-03-18 20:19:19 +00:00
|
|
|
return t, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TokenEntry is used to represent a given token
|
|
|
|
type TokenEntry struct {
|
2015-04-15 21:12:34 +00:00
|
|
|
ID string // ID of this entry, generally 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
|
2015-04-17 18:34:25 +00:00
|
|
|
NumUses int // Used to restrict the number of uses (zero is unlimited). This is to support one-time-tokens (generalized).
|
2015-03-18 20:19:19 +00:00
|
|
|
}
|
|
|
|
|
2015-04-03 18:40:08 +00:00
|
|
|
// 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 (t *TokenStore) SetExpirationManager(exp *ExpirationManager) {
|
|
|
|
t.expiration = exp
|
|
|
|
}
|
|
|
|
|
2015-04-03 00:39:38 +00:00
|
|
|
// 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 {
|
2015-03-18 20:19:19 +00:00
|
|
|
comb := ts.salt + id
|
|
|
|
hash := sha1.Sum([]byte(comb))
|
|
|
|
return hex.EncodeToString(hash[:])
|
|
|
|
}
|
|
|
|
|
2015-03-24 00:16:37 +00:00
|
|
|
// RootToken is used to generate a new token with root privileges and no parent
|
|
|
|
func (ts *TokenStore) RootToken() (*TokenEntry, error) {
|
|
|
|
te := &TokenEntry{
|
2015-04-15 21:24:07 +00:00
|
|
|
Policies: []string{"root"},
|
|
|
|
Path: "auth/token/root",
|
|
|
|
DisplayName: "root",
|
2015-03-24 00:16:37 +00:00
|
|
|
}
|
|
|
|
if err := ts.Create(te); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return te, nil
|
|
|
|
}
|
|
|
|
|
2015-03-18 20:19:19 +00:00
|
|
|
// Create is used to create a new token entry. The entry is assigned
|
2015-03-24 21:22:50 +00:00
|
|
|
// a newly generated ID if not provided.
|
2015-03-18 20:19:19 +00:00
|
|
|
func (ts *TokenStore) Create(entry *TokenEntry) error {
|
2015-04-08 23:43:17 +00:00
|
|
|
defer metrics.MeasureSince([]string{"token", "create"}, time.Now())
|
2015-03-24 21:22:50 +00:00
|
|
|
// Generate an ID if necessary
|
|
|
|
if entry.ID == "" {
|
2015-06-30 19:38:32 +00:00
|
|
|
entry.ID = uuid.GenerateUUID()
|
2015-03-24 21:22:50 +00:00
|
|
|
}
|
2015-04-03 00:39:38 +00:00
|
|
|
saltedId := ts.SaltID(entry.ID)
|
2015-03-24 21:22:50 +00:00
|
|
|
|
|
|
|
// Marshal the entry
|
2015-03-18 20:19:19 +00:00
|
|
|
enc, err := json.Marshal(entry)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to encode entry: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2015-04-03 00:39:38 +00:00
|
|
|
path := parentPrefix + ts.SaltID(entry.Parent) + "/" + saltedId
|
2015-03-18 20:19:19 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2015-04-17 18:51:04 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2015-03-18 20:19:19 +00:00
|
|
|
// Lookup is used to find a token given its ID
|
|
|
|
func (ts *TokenStore) Lookup(id string) (*TokenEntry, error) {
|
2015-04-08 23:43:17 +00:00
|
|
|
defer metrics.MeasureSince([]string{"token", "lookup"}, time.Now())
|
2015-03-18 20:21:16 +00:00
|
|
|
if id == "" {
|
|
|
|
return nil, fmt.Errorf("cannot lookup blank token")
|
|
|
|
}
|
2015-04-03 00:39:38 +00:00
|
|
|
return ts.lookupSalted(ts.SaltID(id))
|
2015-03-18 20:19:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2015-04-08 23:43:17 +00:00
|
|
|
defer metrics.MeasureSince([]string{"token", "revoke"}, time.Now())
|
2015-03-18 20:21:16 +00:00
|
|
|
if id == "" {
|
|
|
|
return fmt.Errorf("cannot revoke blank token")
|
|
|
|
}
|
2015-04-03 00:39:38 +00:00
|
|
|
return ts.revokeSalted(ts.SaltID(id))
|
2015-03-18 20:19:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 != "" {
|
2015-04-03 00:39:38 +00:00
|
|
|
path := parentPrefix + ts.SaltID(entry.Parent) + "/" + saltedId
|
2015-03-18 20:19:19 +00:00
|
|
|
if ts.view.Delete(path); err != nil {
|
|
|
|
return fmt.Errorf("failed to delete entry: %v", err)
|
|
|
|
}
|
|
|
|
}
|
2015-04-10 22:12:04 +00:00
|
|
|
|
|
|
|
// Revoke all secrets under this token
|
|
|
|
if entry != nil {
|
|
|
|
if err := ts.expiration.RevokeByToken(entry.ID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2015-03-18 20:19:19 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RevokeTree is used to invalide a given token and all
|
|
|
|
// child tokens.
|
|
|
|
func (ts *TokenStore) RevokeTree(id string) error {
|
2015-04-08 23:43:17 +00:00
|
|
|
defer metrics.MeasureSince([]string{"token", "revoke-tree"}, time.Now())
|
2015-03-18 20:21:16 +00:00
|
|
|
// Verify the token is not blank
|
|
|
|
if id == "" {
|
|
|
|
return fmt.Errorf("cannot revoke blank token")
|
|
|
|
}
|
|
|
|
|
2015-03-18 20:19:19 +00:00
|
|
|
// Get the salted ID
|
2015-04-03 00:39:38 +00:00
|
|
|
saltedId := ts.SaltID(id)
|
2015-03-18 20:19:19 +00:00
|
|
|
|
2015-04-10 22:06:54 +00:00
|
|
|
// Nuke the entire tree recursively
|
2015-03-18 20:19:19 +00:00
|
|
|
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 {
|
2015-04-10 22:06:54 +00:00
|
|
|
return err
|
2015-03-18 20:19:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-10 22:06:54 +00:00
|
|
|
// Revoke this entry
|
|
|
|
if err := ts.revokeSalted(saltedId); err != nil {
|
|
|
|
return fmt.Errorf("failed to revoke entry: %v", err)
|
2015-03-18 20:19:19 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-03-24 22:10:46 +00:00
|
|
|
// handleCreate handles the auth/token/create path for creation of new tokens
|
2015-03-31 19:48:19 +00:00
|
|
|
func (ts *TokenStore) handleCreate(
|
2015-04-07 21:16:35 +00:00
|
|
|
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
2015-03-24 22:10:46 +00:00
|
|
|
// Read the parent policy
|
|
|
|
parent, err := ts.Lookup(req.ClientToken)
|
|
|
|
if err != nil || parent == nil {
|
|
|
|
return logical.ErrorResponse("parent token lookup failed"), logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
2015-04-17 18:34:25 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2015-03-24 22:10:46 +00:00
|
|
|
// Check if the parent policy is root
|
|
|
|
isRoot := strListContains(parent.Policies, "root")
|
|
|
|
|
|
|
|
// Read and parse the fields
|
2015-04-07 21:16:35 +00:00
|
|
|
var data struct {
|
2015-04-15 21:24:07 +00:00
|
|
|
ID string
|
|
|
|
Policies []string
|
|
|
|
Metadata map[string]string `mapstructure:"meta"`
|
|
|
|
NoParent bool `mapstructure:"no_parent"`
|
|
|
|
Lease string
|
|
|
|
DisplayName string `mapstructure:"display_name"`
|
2015-04-17 18:34:25 +00:00
|
|
|
NumUses int `mapstructure:"num_uses"`
|
2015-04-07 21:16:35 +00:00
|
|
|
}
|
|
|
|
if err := mapstructure.WeakDecode(req.Data, &data); err != nil {
|
|
|
|
return logical.ErrorResponse(fmt.Sprintf(
|
|
|
|
"Error decoding request: %s", err)), logical.ErrInvalidRequest
|
|
|
|
}
|
2015-03-24 22:10:46 +00:00
|
|
|
|
2015-04-17 18:34:25 +00:00
|
|
|
// Verify the number of uses is positive
|
|
|
|
if data.NumUses < 0 {
|
|
|
|
return logical.ErrorResponse("number of uses cannot be negative"),
|
|
|
|
logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
2015-03-24 22:10:46 +00:00
|
|
|
// Setup the token entry
|
|
|
|
te := TokenEntry{
|
2015-04-15 21:24:07 +00:00
|
|
|
Parent: req.ClientToken,
|
|
|
|
Path: "auth/token/create",
|
|
|
|
Meta: data.Metadata,
|
|
|
|
DisplayName: "token",
|
2015-04-17 18:34:25 +00:00
|
|
|
NumUses: data.NumUses,
|
2015-04-15 21:24:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2015-03-24 22:10:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Allow specifying the ID of the token if the client is root
|
2015-04-07 21:16:35 +00:00
|
|
|
if data.ID != "" {
|
2015-03-24 22:10:46 +00:00
|
|
|
if !isRoot {
|
|
|
|
return logical.ErrorResponse("root required to specify token id"),
|
|
|
|
logical.ErrInvalidRequest
|
|
|
|
}
|
2015-04-07 21:16:35 +00:00
|
|
|
te.ID = data.ID
|
2015-03-24 22:10:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Only permit policies to be a subset unless the client is root
|
2015-04-07 21:19:52 +00:00
|
|
|
if len(data.Policies) == 0 {
|
|
|
|
data.Policies = parent.Policies
|
2015-03-24 22:10:46 +00:00
|
|
|
}
|
2015-04-07 21:19:52 +00:00
|
|
|
if !isRoot && !strListSubset(parent.Policies, data.Policies) {
|
|
|
|
return logical.ErrorResponse("child policies must be subset of parent"), logical.ErrInvalidRequest
|
2015-03-24 22:10:46 +00:00
|
|
|
}
|
2015-04-07 21:19:52 +00:00
|
|
|
te.Policies = data.Policies
|
2015-03-24 22:10:46 +00:00
|
|
|
|
|
|
|
// Only allow an orphan token if the client is root
|
2015-04-07 21:16:35 +00:00
|
|
|
if data.NoParent {
|
2015-03-24 22:10:46 +00:00
|
|
|
if !isRoot {
|
|
|
|
return logical.ErrorResponse("root required to create orphan token"),
|
|
|
|
logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
2015-04-07 21:16:35 +00:00
|
|
|
te.Parent = ""
|
2015-03-24 22:10:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the lease if any
|
2015-04-03 21:04:50 +00:00
|
|
|
var leaseDuration time.Duration
|
2015-04-07 21:16:35 +00:00
|
|
|
if data.Lease != "" {
|
|
|
|
dur, err := time.ParseDuration(data.Lease)
|
2015-03-24 22:10:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
if dur < 0 {
|
|
|
|
return logical.ErrorResponse("lease must be positive"), logical.ErrInvalidRequest
|
|
|
|
}
|
2015-04-03 21:04:50 +00:00
|
|
|
leaseDuration = dur
|
2015-03-24 22:10:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create the token
|
|
|
|
if err := ts.Create(&te); err != nil {
|
|
|
|
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate the response
|
|
|
|
resp := &logical.Response{
|
2015-03-31 03:26:39 +00:00
|
|
|
Auth: &logical.Auth{
|
2015-04-26 03:21:35 +00:00
|
|
|
DisplayName: te.DisplayName,
|
|
|
|
Policies: te.Policies,
|
|
|
|
Metadata: te.Meta,
|
2015-04-09 19:14:04 +00:00
|
|
|
LeaseOptions: logical.LeaseOptions{
|
|
|
|
Lease: leaseDuration,
|
|
|
|
LeaseGracePeriod: leaseDuration / 10,
|
|
|
|
Renewable: leaseDuration > 0,
|
|
|
|
},
|
|
|
|
ClientToken: te.ID,
|
2015-03-24 22:10:46 +00:00
|
|
|
},
|
|
|
|
}
|
2015-03-31 03:26:39 +00:00
|
|
|
|
2015-03-24 22:10:46 +00:00
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
2015-03-24 22:30:09 +00:00
|
|
|
// handleRevokeTree handles the auth/token/revoke/id path for revocation of tokens
|
2015-04-08 20:35:32 +00:00
|
|
|
// in a way that revokes all child tokens. Normally, using sys/revoke/leaseID will revoke
|
2015-03-24 22:30:09 +00:00
|
|
|
// the token and all children anyways, but that is only available when there is a lease.
|
2015-03-31 19:48:19 +00:00
|
|
|
func (ts *TokenStore) handleRevokeTree(
|
|
|
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
|
|
id := data.Get("token").(string)
|
2015-03-24 22:30:09 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2015-03-24 22:10:46 +00:00
|
|
|
// handleRevokeOrphan handles the auth/token/revoke-orphan/id path for revocation of tokens
|
2015-04-08 20:35:32 +00:00
|
|
|
// in a way that leaves child tokens orphaned. Normally, using sys/revoke/leaseID will revoke
|
2015-03-24 22:10:46 +00:00
|
|
|
// the token and all children.
|
2015-03-31 19:48:19 +00:00
|
|
|
func (ts *TokenStore) handleRevokeOrphan(
|
|
|
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
2015-03-24 22:30:09 +00:00
|
|
|
// Parse the id
|
2015-03-31 19:48:19 +00:00
|
|
|
id := data.Get("token").(string)
|
2015-03-24 22:30:09 +00:00
|
|
|
if id == "" {
|
|
|
|
return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
// Revoke and orphan
|
|
|
|
if err := ts.Revoke(id); err != nil {
|
|
|
|
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
|
|
|
}
|
2015-03-20 20:54:57 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
2015-03-24 22:10:46 +00:00
|
|
|
|
2015-04-03 18:40:08 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2015-03-24 22:39:33 +00:00
|
|
|
// 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.
|
2015-03-31 19:48:19 +00:00
|
|
|
func (ts *TokenStore) handleLookup(
|
|
|
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
|
|
id := data.Get("token").(string)
|
2015-03-31 19:50:07 +00:00
|
|
|
if id == "" {
|
|
|
|
id = req.ClientToken
|
|
|
|
}
|
2015-03-24 22:39:33 +00:00
|
|
|
if id == "" {
|
|
|
|
return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lookup the token
|
|
|
|
out, err := ts.Lookup(id)
|
2015-06-19 01:30:18 +00:00
|
|
|
|
2015-03-24 22:39:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
if out == nil {
|
2015-06-19 01:30:18 +00:00
|
|
|
return logical.ErrorResponse("bad token"), logical.ErrPermissionDenied
|
2015-03-24 22:39:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a response. We purposely omit the parent reference otherwise
|
|
|
|
// you could escalade your privileges.
|
|
|
|
resp := &logical.Response{
|
|
|
|
Data: map[string]interface{}{
|
2015-04-15 21:24:07 +00:00
|
|
|
"id": out.ID,
|
|
|
|
"policies": out.Policies,
|
|
|
|
"path": out.Path,
|
|
|
|
"meta": out.Meta,
|
|
|
|
"display_name": out.DisplayName,
|
2015-04-17 18:34:25 +00:00
|
|
|
"num_uses": out.NumUses,
|
2015-03-24 22:39:33 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
2015-04-03 19:11:49 +00:00
|
|
|
// 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
|
|
|
|
}
|
2015-04-09 21:23:37 +00:00
|
|
|
incrementRaw := data.Get("increment").(int)
|
|
|
|
|
|
|
|
// Convert the increment
|
|
|
|
increment := time.Duration(incrementRaw) * time.Second
|
2015-04-03 19:11:49 +00:00
|
|
|
|
|
|
|
// Lookup the token
|
|
|
|
out, err := ts.Lookup(id)
|
|
|
|
if err != nil {
|
|
|
|
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the token exists
|
|
|
|
if out == nil {
|
|
|
|
return logical.ErrorResponse("token not found"), logical.ErrInvalidRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
// Revoke the token and its children
|
2015-04-09 21:23:37 +00:00
|
|
|
auth, err := ts.expiration.RenewToken(out.Path, out.ID, increment)
|
2015-04-06 23:35:39 +00:00
|
|
|
if err != nil {
|
2015-04-03 19:11:49 +00:00
|
|
|
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
|
|
|
}
|
2015-04-06 23:35:39 +00:00
|
|
|
|
|
|
|
// Generate the response
|
|
|
|
resp := &logical.Response{
|
|
|
|
Auth: auth,
|
|
|
|
}
|
|
|
|
return resp, nil
|
2015-04-03 19:11:49 +00:00
|
|
|
}
|
|
|
|
|
2015-03-24 22:10:46 +00:00
|
|
|
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.`
|
2015-03-24 22:30:09 +00:00
|
|
|
tokenCreateHelp = `The token create path is used to create new tokens.`
|
2015-03-24 22:39:33 +00:00
|
|
|
tokenLookupHelp = `This endpoint will lookup a token and its properties.`
|
2015-03-24 22:30:09 +00:00
|
|
|
tokenRevokeHelp = `This endpoint will delete the token and all of its child tokens.`
|
|
|
|
tokenRevokeOrphanHelp = `This endpoint will delete the token and orphan its child tokens.`
|
2015-04-03 18:40:08 +00:00
|
|
|
tokenRevokePrefixHelp = `This endpoint will delete all tokens generated under a prefix with their child tokens.`
|
2015-04-03 19:11:49 +00:00
|
|
|
tokenRenewHelp = `This endpoint will renew the token and prevent expiration.`
|
2015-03-24 22:10:46 +00:00
|
|
|
)
|