depot/go/tokend/tokencache.go
Luke Granger-Brown 7592e76a31 tokend: init
tokend is responsible for issuing service-scoped tokens based on the token held
and generated by the Vault Agent.

It can also generate "server-user" scoped tokens, which exist for convenience's
sake: they are not a strong attestation of the user on the machine, and have
limited privileges compared to a Vault token issued using e.g. `vault login
-method=oidc`.
2022-03-20 17:47:52 +00:00

215 lines
4.8 KiB
Go

package main
import (
"context"
"os/user"
"sync"
"time"
log "github.com/golang/glog"
vapi "github.com/hashicorp/vault/api"
)
type TokenSecret = vapi.Secret
type ttledSecret struct {
*TokenSecret
expiration time.Time
isRenewable bool
renewThreshold time.Time
}
func wrapToken(s *TokenSecret) ttledSecret {
now := time.Now()
ttl := s.Auth.LeaseDuration
renewable := s.Auth.Renewable
var ttlBuffer time.Duration
if ttl < 300 {
// Give ourselves an extra two minute buffer for renewal.
ttlBuffer = 2 * time.Minute
}
return ttledSecret{
TokenSecret: s,
expiration: now.Add(time.Duration(ttl) * time.Second),
isRenewable: renewable,
renewThreshold: now.Add(time.Duration(ttl/2) * time.Second).Add(-ttlBuffer),
}
}
func (s ttledSecret) Expired() bool {
// We use !After rather than Before so that if it's _exactly_ now then we still return true.
return !s.expiration.After(time.Now())
}
func (s ttledSecret) ShouldRenew() bool {
if !s.isRenewable {
return false
}
return time.Now().After(s.renewThreshold)
}
type tokenInteractor interface {
Revoke(ctx context.Context, tokenSecret *TokenSecret) error
Issue(ctx context.Context, username string, isPlainUser bool) (*TokenSecret, error)
Renew(ctx context.Context, tokenSecret *TokenSecret) (*TokenSecret, error)
}
type tokenUserCache struct {
l sync.RWMutex
m map[string]ttledSecret
i tokenInteractor
}
func newCache(i tokenInteractor) *tokenUserCache {
return &tokenUserCache{
m: make(map[string]ttledSecret),
i: i,
}
}
func (c *tokenUserCache) expire() (revoke map[string]*TokenSecret) {
c.l.Lock()
defer c.l.Unlock()
var remove []string
revoke = make(map[string]*TokenSecret)
for username, s := range c.m {
// If the token has expired, remove it.
if s.Expired() {
remove = append(remove, username)
continue
}
// If the user no longer exists on the system, revoke it.
if _, err := user.Lookup(username); err != nil {
log.Infof("token for %v will be revoked because user lookup returned error: %v", username, err)
remove = append(remove, username)
revoke[username] = s.TokenSecret
continue
}
// Otherwise, leave it alone.
}
for _, username := range remove {
delete(c.m, username)
}
return revoke
}
func (c *tokenUserCache) renew() {
ctx := context.Background()
toRenew := make(map[string]*TokenSecret)
c.l.Lock()
for username, s := range c.m {
if s.ShouldRenew() {
toRenew[username] = s.TokenSecret
}
}
c.l.Unlock()
for username, s := range toRenew {
log.Infof("renewing token for %v", username)
newS, err := c.i.Renew(ctx, s)
if err != nil {
log.Errorf("renewing token for %v: %w (discarding cached token)", username, err)
// Discard the token in cache.
c.l.Lock()
delete(c.m, username)
c.l.Unlock()
continue
}
c.l.Lock()
if oldS, ok := c.m[username]; ok && oldS.TokenSecret == s {
c.m[username] = wrapToken(newS)
} else if ok {
log.Warningf("after renewing token for %v discovered that the token in cache had changed in the meantime; dropping refreshed token")
}
c.l.Unlock()
}
}
func (c *tokenUserCache) tick(ctx context.Context) error {
log.Info("token cache is ticking")
revoke := c.expire()
for username, tokenSecret := range revoke {
if err := c.i.Revoke(ctx, tokenSecret); err != nil {
log.Errorf("unable to revoke token for %v: %w", username, err)
}
}
c.renew()
return nil
}
func (c *tokenUserCache) Get(ctx context.Context, username string, isPlainUser bool) (*TokenSecret, error) {
c.l.RLock()
token, ok := c.m[username]
c.l.RUnlock()
// If we got a token, but it's expired, delete it and pretend it didn't exist.
if ok && token.Expired() {
c.l.Lock()
delete(c.m, username)
c.l.Unlock()
ok = false
}
if ok {
// We have a non-expired pre-existing token!
return token.TokenSecret, nil
}
// Issue a new token.
issuedToken, err := c.i.Issue(ctx, username, isPlainUser)
if err != nil {
return nil, err
}
// OK, now we check if someone has issued a token for us in the
// meantime... We could (but don't) do any more complex coordination
// than this, so we might actually issue two tokens for the same user
// concurrently. Even if we do this though, one of the tokens will
// "win" and we'll revoke the other one.
c.l.Lock()
defer c.l.Unlock()
if bgToken, ok := c.m[username]; ok && !bgToken.Expired() {
// Just use this one, I guess.
go c.i.Revoke(context.Background(), issuedToken)
return bgToken.TokenSecret, nil
}
token = wrapToken(issuedToken)
c.m[username] = token
return token.TokenSecret, nil
}
func (c *tokenUserCache) Purge(ctx context.Context, username string) {
c.l.Lock()
token, ok := c.m[username]
delete(c.m, username)
c.l.Unlock()
if !ok {
return
}
// Revoke the token as well, for good measure, in case the parent hasn't already done that.
c.i.Revoke(ctx, token.TokenSecret)
}