Do not cache or allow searching for old controllers (#4136)

* Do not cache or allow searching for old controllers

* Identify resourceless responses as coming from supported or unsupported controllers

* Do not report refresh token status info for unsupported controllers
This commit is contained in:
Todd authored and Johan Brandhorst-Satzkorn committed 2024-01-05 07:43:21 -08:00
1 parent d94a25bbd2
commit e429e91bc4
21 files changed
+855 -322

No files matched your search

@@ -6,6 +6,7 @@ package cache
import (
"context"
"database/sql"
stderrors "errors"
"time"
"github.com/hashicorp/boundary/internal/db"
@@ -13,10 +14,70 @@ import (
"github.com/hashicorp/boundary/internal/util"
)
// ErrRefreshNotSupported is returned whenever it is determined a boundary
// instance does not support refresh tokens.
var ErrRefreshNotSupported = stderrors.New("refresh tokens are not supported for this controller")
// RefreshTokenValue is the the type for the actual refresh token value handled
// by the client cache.
type RefreshTokenValue string
// sentinelNoRefreshToken is the value set in the refresh token table when
// Boundary returns resources but no refresh tokens. The presence of this value
// indicates that the boundary instance queried does not support refresh tokens.
const sentinelNoRefreshToken RefreshTokenValue = "__no_refresh_token_supported__"
// CacheSupport is an enum that identifies if a boundary instance can be supported
// by the client daemon cache.
type CacheSupport string
const (
UnknownCacheSupport CacheSupport = "Unknown"
SupportedCacheSupport CacheSupport = "Supported"
NotSupportedCacheSupport CacheSupport = "Not Supported"
)
// cacheSupportCheckedState contains the state of a cache support check.
type cacheSupportCheckedState struct {
supported CacheSupport
lastChecked *time.Time
}
// cacheSupportState returns the cacheSupportCheckedState for the provided user
// which says if caching is supported for that user and when the last time it
// was checked.
func (r *Repository) cacheSupportState(ctx context.Context, u *user) (*cacheSupportCheckedState, error) {
const op = "cache.(Repository).cacheSupportState"
switch {
case util.IsNil(u):
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
case u.Id == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
}
var ret []*refreshToken
if err := r.rw.SearchWhere(ctx, &ret, "user_id = @user_id", []any{sql.Named("user_id", u.Id)}); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if len(ret) == 0 {
return &cacheSupportCheckedState{supported: UnknownCacheSupport}, nil
}
lastChecked := time.Time{}
supported := UnknownCacheSupport
for _, rt := range ret {
if rt.UpdateTime.After(lastChecked) {
lastChecked = rt.UpdateTime
}
switch {
case supported == NotSupportedCacheSupport || rt.RefreshToken == sentinelNoRefreshToken:
supported = NotSupportedCacheSupport
default:
supported = SupportedCacheSupport
}
}
return &cacheSupportCheckedState{supported: supported, lastChecked: &lastChecked}, nil
}
// lookupRefreshToken returns the last known valid refresh token or an empty
// string if one is unkonwn. No error is returned if no valid refresh token is
// found.
@@ -54,7 +115,8 @@ func (r *Repository) listRefreshTokens(ctx context.Context, u *user) ([]*refresh
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
}
var ret []*refreshToken
if err := r.rw.SearchWhere(ctx, &ret, "user_id = @user_id", []any{sql.Named("user_id", u.Id)}); err != nil {
if err := r.rw.SearchWhere(ctx, &ret, "user_id = @user_id and refresh_token != @sentinel",
[]any{sql.Named("user_id", u.Id), sql.Named("sentinel", sentinelNoRefreshToken)}); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return ret, nil
@@ -72,22 +134,40 @@ func (r *Repository) deleteRefreshToken(ctx context.Context, u *user, rType reso
return errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
}
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
rt := &refreshToken{
UserId: u.Id,
ResourceType: rType,
}
n, err := w.Delete(ctx, rt)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if n > 1 {
return errors.New(ctx, errors.MultipleRecords, op, "attempted to delete a single resource but multiple were resource deletions were attempted")
}
return nil
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error {
return deleteRefreshToken(ctx, w, u, rType)
})
if err != nil {
return err
return errors.Wrap(ctx, err, op)
}
return nil
}
// deleteRefreshToken deletes the refresh token for the provided user and resource type
func deleteRefreshToken(ctx context.Context, w db.Writer, u *user, rType resourceType) error {
const op = "cache.deleteRefreshToken"
switch {
case util.IsNil(w):
return errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
case !w.IsTx(ctx):
return errors.New(ctx, errors.InvalidParameter, op, "writer isn't in a transaction")
case util.IsNil(u):
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
case u.Id == "":
return errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
case !rType.valid():
return errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
}
rt := &refreshToken{
UserId: u.Id,
ResourceType: rType,
}
n, err := w.Delete(ctx, rt)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if n > 1 {
return errors.New(ctx, errors.MultipleRecords, op, "attempted to delete a single resource but multiple were resource deletions were attempted")
}
return nil