Files
teleport/lib/backend/backend.go
T
Ghassan Gedeon Achi c1a8522742 Fix Access Graph API sessions expiring at bearer-token TTL instead of session TTL (#66019)
* fix(auth): expire Access Graph API sessions at session TTL, not bearer-token TTL

ACCESS_GRAPH_API web sessions authenticate via the client TLS
certificate in Handler.AuthenticateReqForAccessGraphAPI and never
consume a bearer token. Today they still get one generated and a
paired WebToken upserted. Two consumers
(lib/services/local.webSessions.Upsert computing the backend item
TTL, and lib/web.SessionContext.expired deciding aliveness) then
read the bearer-token expiry directly; for these sessions that's
misleading (the bearer is zero, and expired() silently treats a
zero expiry as "never expires"). This change lifts the usage-aware
choice into api/types.WebSession.GetEarliestExpiry, stops the
producers from generating the bearer artefacts, and updates both
consumers.

Behavioural changes:
  - lib/auth.newWebSession no longer generates a BearerToken or
    BearerTokenExpires for ACCESS_GRAPH_API sessions
  - lib/auth.upsertWebSession skips UpsertWebToken for these sessions
    (no paired WebToken would be meaningful, and creating one with an
    empty token / zero expiry is misleading)

Centralisation:
  - api/types: WebSession.GetEarliestExpiry() returns the time after
    which a session should be considered dead, accounting for
    Usage. Standard sessions return min(bearer, session) with zero
    treated as infinity; ACCESS_GRAPH_API sessions return the
    session expiry. Interface doc spells out the per-usage rule;
    full rationale lives on WebSessionV2.GetEarliestExpiry.
  - lib/services/local.webSessions.Upsert and lib/web
    SessionContext.expired now call session.GetEarliestExpiry()

Tests:
  - api/types: GetEarliestExpiry covers bearer<session,
    bearer>session (capped), zero-bearer, zero-session, and
    ACCESS_GRAPH_API
  - lib/auth: CreateWebSessionFromReq for ACCESS_GRAPH_API leaves
    bearer empty + zero expiry; types.NewWebToken's empty-token
    validation means a successful call also proves UpsertWebToken
    was skipped. UNSPECIFIED still populates bearer and upserts the
    paired WebToken.
  - lib/services/local: backend item Expires matches the per-usage
    expiry choice through Upsert

* fix: remove backend.EarliestExpiry and update doc
2026-04-23 17:35:58 +00:00

426 lines
13 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package backend provides storage backend abstraction layer
package backend
import (
"context"
"fmt"
"iter"
"time"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/gravitational/teleport/api/types"
)
// Forever means that object TTL will not expire unless deleted
const (
Forever time.Duration = 0
)
// ErrIncorrectRevision is returned from conditional operations when revisions
// do not match the expected value.
var ErrIncorrectRevision = &trace.CompareFailedError{Message: "resource revision does not match, it may have been concurrently created|modified|deleted; please work from the latest state, or use --force to overwrite"}
// Backend implements abstraction over local or remote storage backend.
// Item keys are assumed to be valid UTF8, which may be enforced by the
// various Backend implementations.
type Backend interface {
// GetName returns the implementation driver name.
GetName() string
// Create creates item if it does not exist
Create(ctx context.Context, i Item) (*Lease, error)
// Put puts value into backend (creates if it does not
// exists, updates it otherwise)
Put(ctx context.Context, i Item) (*Lease, error)
// CompareAndSwap compares item with existing item
// and replaces is with replaceWith item
CompareAndSwap(ctx context.Context, expected Item, replaceWith Item) (*Lease, error)
// Update updates value in the backend
Update(ctx context.Context, i Item) (*Lease, error)
// Get returns a single item or not found error
Get(ctx context.Context, key Key) (*Item, error)
// Items produces an iterator of backend items in the range, and order
// described in the provided [ItemsParams].
Items(ctx context.Context, params ItemsParams) iter.Seq2[Item, error]
// GetRange returns the items between the start and end keys, including both
// (if present).
GetRange(ctx context.Context, startKey, endKey Key, limit int) (*GetResult, error)
// Delete deletes item by key, returns NotFound error
// if item does not exist
Delete(ctx context.Context, key Key) error
// DeleteRange deletes range of items with keys between startKey and endKey
DeleteRange(ctx context.Context, startKey, endKey Key) error
// KeepAlive keeps object from expiring, updates lease on the existing object,
// expires contains the new expiry to set on the lease,
// some backends may ignore expires based on the implementation
// in case if the lease managed server side
KeepAlive(ctx context.Context, lease Lease, expires time.Time) error
// ConditionalUpdate updates the value in the backend if the revision of the [Item] matches
// the stored revision.
ConditionalUpdate(ctx context.Context, i Item) (*Lease, error)
// ConditionalDelete deletes the item by key if the revision matches the stored revision.
ConditionalDelete(ctx context.Context, key Key, revision string) error
// AtomicWrite executes a batch of conditional actions atomically s.t. all actions happen if all
// conditions are met, but no actions happen if any condition fails to hold. If one or more conditions
// failed to hold, [ErrConditionFailed] is returned. The number of conditional actions must not
// exceed [MaxAtomicWriteSize] and no two conditional actions may point to the same key. If successful,
// the returned revision is the new revision associated with all [Put] actions that were part of the
// operation (the revision value has no meaning outside of the context of puts).
AtomicWrite(ctx context.Context, condacts []ConditionalAction) (revision string, err error)
// NewWatcher returns a new event watcher
NewWatcher(ctx context.Context, watch Watch) (Watcher, error)
// Close closes backend and all associated resources
Close() error
// Clock returns clock used by this backend
Clock() clockwork.Clock
// CloseWatchers closes all the watchers
// without closing the backend
CloseWatchers()
}
// ItemsParams are parameters that are provided to
// [BackendWithItems.Items] to alter the iteration behavior.
type ItemsParams struct {
// StartKey is the minimum key in the range yielded by the iteration. This key
// will be included in the results if it exists.
StartKey Key
// EndKey is the maximum key in the range yielded by the iteration. This key
// will be included in the results if it exists.
EndKey Key
// Descending makes the iteration yield items from the biggest to the smallest
// key (i.e. from EndKey to StartKey). If unset, the iteration will proceed in the
// usual ascending order (i.e. from StartKey to EndKey).
Descending bool
// Limit is an optional maximum number of items to retrieve during iteration.
Limit int
}
// New initializes a new [Backend] implementation based on the service config.
func New(ctx context.Context, backend string, params Params) (Backend, error) {
registryMu.RLock()
defer registryMu.RUnlock()
newbk, ok := registry[backend]
if !ok {
return nil, trace.BadParameter("unsupported secrets storage type: %q", backend)
}
bk, err := newbk(ctx, params)
if err != nil {
return nil, trace.Wrap(err)
}
return bk, nil
}
// Lease represents a lease on the item that can be used
// to extend item's TTL without updating its contents.
//
// Here is an example of renewing object TTL:
//
// item.Expires = time.Now().Add(10 * time.Second)
// lease, err := backend.Create(ctx, item)
// expires := time.Now().Add(20 * time.Second)
// err = backend.KeepAlive(ctx, lease, expires)
type Lease struct {
// Key is the resource identifier.
Key Key
// Revision is the last known version of the object.
Revision string
}
// Watch specifies watcher parameters
type Watch struct {
// Name is a watch name set for debugging
// purposes
Name string
// Prefixes specifies prefixes to watch,
// passed to the backend implementation
Prefixes []Key
// QueueSize is an optional queue size
QueueSize int
// MetricComponent if set will start reporting
// with a given component metric
MetricComponent string
}
// String returns a user-friendly description
// of the watcher
func (w *Watch) String() string {
return fmt.Sprintf("Watcher(name=%v, prefixes=%v)", w.Name, w.Prefixes)
}
// Watcher returns watcher
type Watcher interface {
// Events returns channel with events
Events() <-chan Event
// Done returns the channel signaling the closure
Done() <-chan struct{}
// Close closes the watcher and releases
// all associated resources
Close() error
}
// GetResult provides the result of GetRange request
type GetResult struct {
// Items returns a list of items
Items []Item
}
// Event is a event containing operation with item
type Event struct {
// Type is operation type
Type types.OpType
// Item is event Item
Item Item
}
// Item is a key value item
type Item struct {
// Key is a key of the key value item
Key Key
// Value is a value of the key value item
Value []byte
// Expires is an optional record expiry time
Expires time.Time
// Revision is the last known version of the object.
Revision string
}
func (e Event) String() string {
val := string(e.Item.Value)
if len(val) > 20 {
val = val[:20] + "..."
}
return fmt.Sprintf("%v %s=%s", e.Type, e.Item.Key, val)
}
// Config is used for 'storage' config section. It's a combination of
// values for various backends: 'etcd', 'filesystem', 'dynamodb', etc.
type Config struct {
// Type indicates which backend to use (etcd, dynamodb, etc)
Type string `yaml:"type,omitempty"`
// Params is a generic key/value property bag which allows arbitrary
// values to be passed to backend
Params Params `yaml:",inline"`
}
// Params type defines a flexible unified back-end configuration API.
// It is just a map of key/value pairs which gets populated by `storage` section
// in Teleport YAML config.
type Params map[string]any
// GetString returns a string value stored in Params map, or an empty string
// if nothing is found
func (p Params) GetString(key string) string {
v, ok := p[key]
if !ok {
return ""
}
s, _ := v.(string)
return s
}
// NoLimit specifies no limits
const NoLimit = 0
const noEnd = "\x00"
// RangeEnd returns end of the range for given key.
func RangeEnd(key Key) Key {
end := make([]byte, len(key.s))
copy(end, key.s)
for i := len(end) - 1; i >= 0; i-- {
if end[i] < 0xff {
end[i] = end[i] + 1
end = end[:i+1]
return KeyFromString(string(end))
}
}
// next key does not exist (e.g., 0xffff);
return Key{noEnd: true}
}
// HostID is a derivation of a KeyedItem that allows the host id
// to be included in the key.
type HostID interface {
KeyedItem
GetHostID() string
}
// KeyedItem represents an item from which a pagination key can be derived.
type KeyedItem interface {
GetName() string
}
// GetPaginationKey returns the pagination key given item.
// For items that implement HostID, the next key will also
// have the HostID part.
func GetPaginationKey(ki KeyedItem) string {
if h, ok := ki.(HostID); ok {
return h.GetHostID() + SeparatorString + h.GetName()
}
return ki.GetName()
}
// MaskKeyName masks the given key name.
// e.g "123456789" -> "******789"
func MaskKeyName(keyName string) string {
maskedBytes := []byte(keyName)
hiddenBefore := int(0.75 * float64(len(keyName)))
for i := range hiddenBefore {
maskedBytes[i] = '*'
}
return string(maskedBytes)
}
// Items is a sortable list of backend items
type Items []Item
// Len is part of sort.Interface.
func (it Items) Len() int {
return len(it)
}
// Swap is part of sort.Interface.
func (it Items) Swap(i, j int) {
it[i], it[j] = it[j], it[i]
}
// Less is part of sort.Interface.
func (it Items) Less(i, j int) bool {
return it[i].Key.Compare(it[j].Key) < 0
}
// TTL returns TTL in duration units, rounds up to one second
func TTL(clock clockwork.Clock, expires time.Time) time.Duration {
ttl := expires.Sub(clock.Now())
if ttl < time.Second {
return time.Second
}
return ttl
}
// Expiry converts ttl to expiry time, if ttl is 0
// returns empty time
func Expiry(clock clockwork.Clock, ttl time.Duration) time.Time {
if ttl == 0 {
return time.Time{}
}
return clock.Now().UTC().Add(ttl)
}
// CreateRevision generates a new identifier to be used
// as a resource revision. Backend implementations that provide
// their own mechanism for versioning resources should be
// preferred.
func CreateRevision() string {
return uuid.NewString()
}
// BlankRevision is a placeholder revision to be used by backends when
// the revision of the item in the backend is empty. This can happen
// to any existing resources that were last written before support for
// revisions was added.
var BlankRevision = uuid.Nil.String()
// NewLease creates a lease for the provided [Item].
func NewLease(item Item) *Lease {
return &Lease{
Key: item.Key,
Revision: item.Revision,
}
}
// BatchPutter is an optional interface that backends can implement
// to support batched PutBatch operations for improved performance when writing
// multiple items at once.
type BatchPutter interface {
// PutBatch upserts multiple items into the backend in a single call, in a way
// that is equivalent to a loop around multiple invocations of [backend.Put],
// but with the potential to be more efficient or faster, depending on the
// implementation. Returns a revision item for each item in the same order.
// Revisions are not guaranteed to be different nor they are guaranteed to be
// the same between items of the same batch. If an error is returned, it's
// possible for some of the items to have been persisted to the storage. The
// order in which items are internally persisted is an implementation detail.
PutBatch(context.Context, []Item) ([]string, error)
}
// PutBatch is an implementation of PutBatch that by default calls Put for each item.
// Backends can overwrite this behavior providing optimized PutBatch implementation.
//
// WARNING: Make sure that items have unique keys when calling PutBatch.
func PutBatch(ctx context.Context, bk Backend, items []Item) ([]string, error) {
if v, hasDuplicate := hasDuplicateKeys(items); hasDuplicate {
return nil, trace.BadParameter("duplicate key detected in PutBatch: %q", v)
}
// Many Backend implementations rely on unique keys for correct operation.
// Where it is up to the caller to ensure this to remove duplication keys
// Just in case we will fallback to single Put calls if duplicates are detected.
if v, ok := bk.(BatchPutter); ok {
revs, err := v.PutBatch(ctx, items)
return revs, trace.Wrap(err)
}
revisions := make([]string, 0, len(items))
for _, item := range items {
rev, err := bk.Put(ctx, item)
if err != nil {
return nil, trace.Wrap(err)
}
revisions = append(revisions, rev.Revision)
}
return revisions, nil
}
func hasDuplicateKeys(items Items) (string, bool) {
seen := make(map[string]struct{})
for _, ca := range items {
keyStr := ca.Key.String()
if _, ok := seen[keyStr]; ok {
return keyStr, true
}
seen[keyStr] = struct{}{}
}
return "", false
}