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

+25 -35
View File
@@ -6,6 +6,7 @@ package cache
import (
"context"
stderrors "errors"
"fmt"
"time"
"github.com/hashicorp/boundary/api"
@@ -114,32 +115,13 @@ func (r *RefreshService) refreshableUsers(ctx context.Context, in []*user) ([]*u
const op = "cache.(RefreshService).refreshableUsers"
var ret []*user
for _, u := range in {
rt, err := r.repo.listRefreshTokens(ctx, u)
cs, err := r.repo.cacheSupportState(ctx, u)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if len(rt) > 0 {
if cs.supported != NotSupportedCacheSupport {
ret = append(ret, u)
continue
}
// If any resource exists for this user, since it doesn't have a refresh
// token yet it is not refreshable.
ts, err := r.repo.searchTargets(ctx, "true", nil, withUserId(u.Id))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if len(ts) > 0 {
continue
}
ss, err := r.repo.searchSessions(ctx, "true", nil, withUserId(u.Id))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if len(ss) > 0 {
continue
}
ret = append(ret, u)
}
return ret, nil
}
@@ -267,24 +249,24 @@ func (r *RefreshService) Refresh(ctx context.Context, opt ...Option) error {
}
if err := r.repo.refreshTargets(ctx, u, tokens, opt...); err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op))
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for user id %s", u.Id))))
}
if err := r.repo.refreshSessions(ctx, u, tokens, opt...); err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op))
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for user id %s", u.Id))))
}
}
return retErr
}
// FullFetch is used for users of boundary instances which do not support
// refresh tokens. For these users a call to FullFetch retrieves all resources
// for that user and updates the database with the result. Users are identified
// as being in this group if they have at least one resource in the cache and
// do not have any refresh tokens. Users who do not have any resources nor
// refresh tokens are updated using Refresh.
func (r *RefreshService) FullFetch(ctx context.Context, opt ...Option) error {
const op = "cache.(RefreshService).Refresh"
// RecheckCachingSupport is used for users of boundary instances which do not
// support refresh tokens. For these users a call to RecheckCachingSupport
// retrieves resources for that user to see if the controller now returns a
// refresh token. Users who do not have any resources nor refresh tokens still
// are Refreshed. If caching moves from unsupported to supported this method
// will cache the results automatically.
func (r *RefreshService) RecheckCachingSupport(ctx context.Context, opt ...Option) error {
const op = "cache.(RefreshService).RecheckCachingSupport"
if err := r.repo.cleanExpiredOrOrphanedAuthTokens(ctx); err != nil {
return errors.Wrap(ctx, err, op)
}
@@ -318,11 +300,19 @@ func (r *RefreshService) FullFetch(ctx context.Context, opt ...Option) error {
continue
}
if err := r.repo.fullFetchTargets(ctx, u, tokens, opt...); err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op))
if err := r.repo.checkCachingTargets(ctx, u, tokens, opt...); err != nil {
if err == ErrRefreshNotSupported {
// This is expected so no need to propogate the error up
continue
}
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for user id %s", u.Id))))
}
if err := r.repo.fullFetchSessions(ctx, u, tokens, opt...); err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op))
if err := r.repo.checkCachingSessions(ctx, u, tokens, opt...); err != nil {
if err == ErrRefreshNotSupported {
// This is expected so no need to propogate the error up
continue
}
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for user id %s", u.Id))))
}
}
+76 -79
View File
@@ -52,11 +52,11 @@ func testStaticResourceRetrievalFunc[T any](t *testing.T, ret [][]T, removed [][
}
}
// testFullFetchRetrievalFunc simulates a controller that doesn't support refresh
// testNoRefreshRetrievalFunc simulates a controller that doesn't support refresh
// since it does not return any refresh token.
func testFullFetchRetrievalFunc[T any](t *testing.T, ret []T) func(context.Context, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
func testNoRefreshRetrievalFunc[T any](t *testing.T) func(context.Context, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
return func(_ context.Context, _, _ string, _ RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
return ret, nil, "", nil
return nil, nil, "", ErrRefreshNotSupported
}
}
@@ -460,45 +460,52 @@ func TestRefreshForSearch(t *testing.T) {
require.NoError(t, err)
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
require.NoError(t, err)
rs, err := NewRefreshService(ctx, r, time.Millisecond, 0)
rs, err := NewRefreshService(ctx, r, 0, 0)
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
retTargets := []*targets.Target{
target("1"),
target("2"),
target("3"),
target("4"),
}
// Get the first set of resources, but no refresh tokens
assert.NoError(t, rs.Refresh(ctx,
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, nil)),
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, retTargets[:2]))))
err = rs.Refresh(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.ErrorContains(t, err, ErrRefreshNotSupported.Error())
got, err := r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retTargets[:2], got)
assert.Empty(t, got)
// Let 2 milliseconds pass so the items are stale enough
time.Sleep(2 * time.Millisecond)
// Now that we know that this user doesn't support refresh tokens, they
// wont be refreshed any more, and we wont see the error when refreshing
// any more.
err = rs.Refresh(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.Nil(t, err)
err = rs.RecheckCachingSupport(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.Nil(t, err)
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Targets,
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, nil)),
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, retTargets))))
got, err = r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
// still no change in targets since they weren't refreshed with that search
// due to having no refresh token.
assert.ElementsMatch(t, retTargets[:2], got)
assert.Empty(t, got)
// Now simulate the controller updating to support refresh tokens and
// the resources starting to be cached.
err = rs.RecheckCachingSupport(ctx,
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, [][]*targets.Target{retTargets}, [][]string{{}})))
assert.Nil(t, err, err)
// Only a full fetch will get all the resources
assert.NoError(t, rs.FullFetch(ctx,
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, nil)),
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, retTargets))))
got, err = r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, got, retTargets)
assert.Len(t, got, 2)
})
t.Run("sessions refreshed for searching", func(t *testing.T) {
@@ -766,7 +773,7 @@ func TestRefresh(t *testing.T) {
})
}
func TestFullFetch(t *testing.T) {
func TestRecheckCachingSupport(t *testing.T) {
ctx := context.Background()
boundaryAddr := "address"
@@ -783,7 +790,7 @@ func TestFullFetch(t *testing.T) {
atMap[ringToken{"k", "t"}] = at
t.Run("set targets", func(t *testing.T) {
t.Run("targets", func(t *testing.T) {
s, err := db.Open(ctx)
require.NoError(t, err)
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
@@ -792,41 +799,32 @@ func TestFullFetch(t *testing.T) {
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
retTargets := []*targets.Target{
target("1"),
target("2"),
target("3"),
target("4"),
}
// Since this user doesn't have any resources, the user's data will still
// only get updated with a call to Refresh.
assert.NoError(t, rs.FullFetch(ctx,
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, nil)),
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, retTargets))))
assert.NoError(t, rs.RecheckCachingSupport(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t))))
got, err := r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, got)
assert.NoError(t, rs.Refresh(ctx,
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, nil)),
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, retTargets[:2]))))
err = rs.Refresh(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.ErrorIs(t, err, ErrRefreshNotSupported)
got, err = r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, got, retTargets[:2])
assert.Empty(t, got)
// now a full fetch will work since the user has resources and no refresh token
assert.NoError(t, rs.FullFetch(ctx,
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, nil)),
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, retTargets))))
got, err = r.ListTargets(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, got, retTargets)
assert.NoError(t, rs.RecheckCachingSupport(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t))))
})
t.Run("set sessions", func(t *testing.T) {
t.Run("sessions", func(t *testing.T) {
s, err := db.Open(ctx)
require.NoError(t, err)
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
@@ -835,33 +833,29 @@ func TestFullFetch(t *testing.T) {
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
retSess := []*sessions.Session{
session("1"),
session("2"),
session("3"),
session("4"),
}
assert.NoError(t, rs.FullFetch(ctx,
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, nil)),
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, retSess))))
assert.NoError(t, rs.RecheckCachingSupport(ctx,
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
got, err := r.ListSessions(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, got)
assert.NoError(t, rs.Refresh(ctx,
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, nil)),
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, retSess[:2]))))
got, err = r.ListSessions(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, got, retSess[:2])
err = rs.Refresh(ctx,
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
assert.ErrorIs(t, err, ErrRefreshNotSupported)
assert.NoError(t, rs.FullFetch(ctx,
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, nil)),
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, retSess))))
got, err = r.ListSessions(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, got, retSess)
assert.Empty(t, got)
assert.NoError(t, rs.RecheckCachingSupport(ctx,
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
got, err = r.ListSessions(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, got)
})
t.Run("error propogates up", func(t *testing.T) {
@@ -873,13 +867,14 @@ func TestFullFetch(t *testing.T) {
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
assert.NoError(t, rs.Refresh(ctx,
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, []*targets.Target{target("1")})),
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, []*sessions.Session{session("1")}))))
err = rs.Refresh(ctx,
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
assert.ErrorIs(t, err, ErrRefreshNotSupported)
innerErr := errors.New("test error")
err = rs.FullFetch(ctx,
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, nil)),
err = rs.RecheckCachingSupport(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string, refreshTok RefreshTokenValue) ([]*targets.Target, []string, RefreshTokenValue, error) {
require.Equal(t, boundaryAddr, addr)
require.Equal(t, at.Token, token)
@@ -887,9 +882,9 @@ func TestFullFetch(t *testing.T) {
}))
assert.ErrorContains(t, err, innerErr.Error())
err = rs.FullFetch(ctx,
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, nil)),
WithSessionRetrievalFunc(func(ctx context.Context, addr, token string, refreshTok RefreshTokenValue) ([]*sessions.Session, []string, RefreshTokenValue, error) {
err = rs.RecheckCachingSupport(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string, refreshTok RefreshTokenValue) ([]*targets.Target, []string, RefreshTokenValue, error) {
require.Equal(t, boundaryAddr, addr)
require.Equal(t, at.Token, token)
return nil, nil, "", innerErr
@@ -906,9 +901,10 @@ func TestFullFetch(t *testing.T) {
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
assert.NoError(t, rs.Refresh(ctx,
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, []*targets.Target{target("1")})),
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, []*sessions.Session{session("1")}))))
err = rs.Refresh(ctx,
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
assert.ErrorIs(t, err, ErrRefreshNotSupported)
// Remove the token from the keyring, see that we can still see the
// token and then user until a Refresh happens which causes them to be
@@ -923,9 +919,10 @@ func TestFullFetch(t *testing.T) {
require.NoError(t, err)
assert.Len(t, us, 1)
rs.FullFetch(ctx,
WithSessionRetrievalFunc(testFullFetchRetrievalFunc[*sessions.Session](t, nil)),
WithTargetRetrievalFunc(testFullFetchRetrievalFunc[*targets.Target](t, nil)))
err = rs.RecheckCachingSupport(ctx,
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.NoError(t, err)
ps, err = r.listTokens(ctx, u)
require.NoError(t, err)
@@ -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
@@ -55,6 +55,20 @@ func TestListRefreshToken(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, got, 1)
})
t.Run("sentinel values", func(t *testing.T) {
known := &user{Id: "sentinelValues", Address: "addr"}
require.NoError(t, r.rw.Create(ctx, known))
token := sentinelNoRefreshToken
r.rw.DoTx(ctx, 1, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
require.NoError(t, upsertRefreshToken(ctx, w, known, targetResourceType, token))
return nil
})
got, err := r.listRefreshTokens(ctx, known)
assert.NoError(t, err)
assert.Empty(t, got)
})
t.Run("got multiple responses", func(t *testing.T) {
known := &user{Id: "with multiple responses", Address: "addr"}
require.NoError(t, r.rw.Create(ctx, known))
@@ -72,6 +86,76 @@ func TestListRefreshToken(t *testing.T) {
})
}
func TestCacheSupportState(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
r, err := NewRepository(ctx, s, &sync.Map{},
mapBasedAuthTokenKeyringLookup(map[ringToken]*authtokens.AuthToken{}),
sliceBasedAuthTokenBoundaryReader(nil))
require.NoError(t, err)
t.Run("unknown", func(t *testing.T) {
known := &user{Id: "unknown", Address: "addr"}
require.NoError(t, r.rw.Create(ctx, known))
got, err := r.listRefreshTokens(ctx, known)
assert.NoError(t, err)
assert.Empty(t, got)
cs, err := r.cacheSupportState(ctx, known)
assert.NoError(t, err)
assert.Equal(t, cs.supported, UnknownCacheSupport)
assert.Nil(t, cs.lastChecked)
})
t.Run("unsupported", func(t *testing.T) {
known := &user{Id: "unsupported", Address: "addr"}
require.NoError(t, r.rw.Create(ctx, known))
now := time.Now().Truncate(time.Millisecond)
token := sentinelNoRefreshToken
r.rw.DoTx(ctx, 1, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
require.NoError(t, upsertRefreshToken(ctx, w, known, targetResourceType, token))
return nil
})
got, err := r.listRefreshTokens(ctx, known)
assert.NoError(t, err)
assert.Empty(t, got)
cs, err := r.cacheSupportState(ctx, known)
assert.NoError(t, err)
assert.Equal(t, cs.supported, NotSupportedCacheSupport)
assert.NotNil(t, cs.lastChecked)
assert.LessOrEqual(t, now, *cs.lastChecked)
})
t.Run("Supported", func(t *testing.T) {
known := &user{Id: "supported", Address: "addr"}
require.NoError(t, r.rw.Create(ctx, known))
now := time.Now().Truncate(time.Millisecond)
token := RefreshTokenValue("something")
r.rw.DoTx(ctx, 1, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
require.NoError(t, upsertRefreshToken(ctx, w, known, targetResourceType, token))
require.NoError(t, upsertRefreshToken(ctx, w, known, sessionResourceType, token))
return nil
})
got, err := r.listRefreshTokens(ctx, known)
assert.NoError(t, err)
assert.Len(t, got, 2)
cs, err := r.cacheSupportState(ctx, known)
assert.NoError(t, err)
assert.Equal(t, cs.supported, SupportedCacheSupport)
assert.NotNil(t, cs.lastChecked)
assert.LessOrEqual(t, now, *cs.lastChecked)
})
}
func TestLookupRefreshToken(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
+63 -25
View File
@@ -40,6 +40,9 @@ func defaultSessionFunc(ctx context.Context, addr, authTok string, refreshTok Re
}
return nil, nil, "", errors.Wrap(ctx, err, op)
}
if l.ResponseType == "" {
return nil, nil, "", ErrRefreshNotSupported
}
return l.Items, l.RemovedIds, RefreshTokenValue(l.ListToken), nil
}
@@ -78,6 +81,7 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
var gotResponse bool
var resp []*sessions.Session
var newRefreshToken RefreshTokenValue
var unsupportedCacheRequest bool
var removedIds []string
var retErr error
for at, t := range tokens {
@@ -91,8 +95,12 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
resp, removedIds, newRefreshToken, err = opts.withSessionRetrievalFunc(ctx, u.Address, t, "")
}
if err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
continue
if err == ErrRefreshNotSupported {
unsupportedCacheRequest = true
} else {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
continue
}
}
gotResponse = true
break
@@ -121,26 +129,37 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
return err
}
}
if err := upsertSessions(ctx, w, u, resp); err != nil {
return err
}
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
return err
switch {
case unsupportedCacheRequest:
if err := upsertRefreshToken(ctx, w, u, resourceType, sentinelNoRefreshToken); err != nil {
return err
}
case newRefreshToken != "":
if err := upsertSessions(ctx, w, u, resp); err != nil {
return err
}
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
return err
}
default:
// controller supports caching, but doesn't have any resources
}
return nil
})
if err != nil {
return errors.Wrap(ctx, err, op)
}
if unsupportedCacheRequest {
return ErrRefreshNotSupported
}
return nil
}
// fullFetchSessions fetches all sessions for the provided user and sets the
// checkCachingSessions fetches all sessions for the provided user and sets the
// cache to match the values returned. If the response includes a refresh
// token it will save that as well.
func (r *Repository) fullFetchSessions(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
const op = "cache.(Repository).fullFetchSessions"
func (r *Repository) checkCachingSessions(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
const op = "cache.(Repository).checkSessionsForSearchability"
switch {
case util.IsNil(u):
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
@@ -163,12 +182,17 @@ func (r *Repository) fullFetchSessions(ctx context.Context, u *user, tokens map[
var gotResponse bool
var resp []*sessions.Session
var newRefreshToken RefreshTokenValue
var unsupportedCacheRequest bool
var retErr error
for at, t := range tokens {
resp, _, newRefreshToken, err = opts.withSessionRetrievalFunc(ctx, u.Address, t, "")
if err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
continue
if err == ErrRefreshNotSupported {
unsupportedCacheRequest = true
} else {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
continue
}
}
gotResponse = true
break
@@ -183,24 +207,38 @@ func (r *Repository) fullFetchSessions(ctx context.Context, u *user, tokens map[
return retErr
}
event.WriteSysEvent(ctx, op, fmt.Sprintf("updating %d sessions for user %v", len(resp), u))
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error {
if _, err := w.Exec(ctx, "delete from session where fk_user_id = @fk_user_id",
[]any{sql.Named("fk_user_id", u.Id)}); err != nil {
return err
}
if err := upsertSessions(ctx, w, u, resp); err != nil {
return err
}
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
return err
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error {
switch {
case unsupportedCacheRequest:
if err := upsertRefreshToken(ctx, w, u, resourceType, sentinelNoRefreshToken); err != nil {
return err
}
case newRefreshToken != "":
if _, err := w.Exec(ctx, "delete from session where fk_user_id = @fk_user_id",
[]any{sql.Named("fk_user_id", u.Id)}); err != nil {
return err
}
if err := upsertSessions(ctx, w, u, resp); err != nil {
return err
}
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
return err
}
default:
// This is no longer flagged as not supported, but we dont have a
// refresh token so clear out any refresh token we have stored.
if err := deleteRefreshToken(ctx, w, u, resourceType); err != nil {
return err
}
}
return nil
})
if err != nil {
return errors.Wrap(ctx, err, op)
}
if unsupportedCacheRequest {
return ErrRefreshNotSupported
}
return nil
}
+72 -31
View File
@@ -40,6 +40,9 @@ func defaultTargetFunc(ctx context.Context, addr, authTok string, refreshTok Ref
}
return nil, nil, "", errors.Wrap(ctx, err, op)
}
if l.ResponseType == "" {
return nil, nil, "", ErrRefreshNotSupported
}
return l.Items, l.RemovedIds, RefreshTokenValue(l.ListToken), nil
}
@@ -77,21 +80,26 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
var gotResponse bool
var resp []*targets.Target
var removedIds []string
var newRefreshTokenVal RefreshTokenValue
var newRefreshToken RefreshTokenValue
var unsupportedCacheRequest bool
var retErr error
for at, t := range tokens {
resp, removedIds, newRefreshTokenVal, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, oldRefreshTokenVal)
resp, removedIds, newRefreshToken, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, oldRefreshTokenVal)
if api.ErrInvalidListToken.Is(err) {
if err := r.deleteRefreshToken(ctx, u, resourceType); err != nil {
return errors.Wrap(ctx, err, op)
}
// try again without the refresh token
oldRefreshToken = nil
resp, removedIds, newRefreshTokenVal, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, "")
resp, removedIds, newRefreshToken, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, "")
}
if err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
continue
if err == ErrRefreshNotSupported {
unsupportedCacheRequest = true
} else {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
continue
}
}
gotResponse = true
break
@@ -106,9 +114,9 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
}
event.WriteSysEvent(ctx, op, fmt.Sprintf("updating %d targets for user %v", len(resp), u))
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error {
switch {
case oldRefreshToken == nil:
case oldRefreshToken == nil || unsupportedCacheRequest:
if _, err := w.Exec(ctx, "delete from target where fk_user_id = @fk_user_id",
[]any{sql.Named("fk_user_id", u.Id)}); err != nil {
return err
@@ -119,28 +127,40 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
return err
}
}
if err := upsertTargets(ctx, w, u, resp); err != nil {
return err
}
if newRefreshTokenVal != "" || oldRefreshToken != nil {
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshTokenVal); err != nil {
switch {
case unsupportedCacheRequest:
if err := upsertRefreshToken(ctx, w, u, resourceType, sentinelNoRefreshToken); err != nil {
return err
}
case newRefreshToken != "":
if err := upsertTargets(ctx, w, u, resp); err != nil {
return err
}
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
return err
}
default:
// controller supports caching, but doesn't have any resources
}
return nil
})
if err != nil {
return errors.Wrap(ctx, err, op)
}
if unsupportedCacheRequest {
return ErrRefreshNotSupported
}
return nil
}
// fullFetchTargets fetches all targets for the provided user and sets the
// cache to match the values returned. If the response includes a refresh
// token it will save that as well.
func (r *Repository) fullFetchTargets(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
const op = "cache.(Repository).fullFetchTargets"
// checkCachingTargets fetches all targets for the provided user. If the
// response has at least one target and a refresh token, it makes the targets
// cachable and stores the refresh token. If there is no refresh token in the
// response it marks this user as unable to cache the data. If no data and no
// refresh token is stored it is unknown if the targets are cachable, the user
// is not marked as unknown.
func (r *Repository) checkCachingTargets(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
const op = "cache.(Repository).checkCachingTargets"
switch {
case util.IsNil(u):
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
@@ -161,12 +181,17 @@ func (r *Repository) fullFetchTargets(ctx context.Context, u *user, tokens map[A
var gotResponse bool
var resp []*targets.Target
var newRefreshToken RefreshTokenValue
var unsupportedCacheRequest bool
var retErr error
for at, t := range tokens {
resp, _, newRefreshToken, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, "")
if err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
continue
if err == ErrRefreshNotSupported {
unsupportedCacheRequest = true
} else {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg("for token %q", at.Id)))
continue
}
}
gotResponse = true
break
@@ -180,26 +205,42 @@ func (r *Repository) fullFetchTargets(ctx context.Context, u *user, tokens map[A
return retErr
}
event.WriteSysEvent(ctx, op, fmt.Sprintf("updating %d targets for user %v", len(resp), u))
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
if _, err := w.Exec(ctx, "delete from target where fk_user_id = @fk_user_id",
[]any{sql.Named("fk_user_id", u.Id)}); err != nil {
return err
}
if err := upsertTargets(ctx, w, u, resp); err != nil {
return err
}
if newRefreshToken != "" {
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error {
switch {
case unsupportedCacheRequest:
// Since we know the controller doesn't support caching, we mark the
// user as unable to cache the data.
if err := upsertRefreshToken(ctx, w, u, resourceType, sentinelNoRefreshToken); err != nil {
return err
}
case newRefreshToken != "":
// Now that there is a refresh token, the data can be cached, so
// cache it and store the refresh token for future refreshes.
if _, err := w.Exec(ctx, "delete from target where fk_user_id = @fk_user_id",
[]any{sql.Named("fk_user_id", u.Id)}); err != nil {
return err
}
if err := upsertTargets(ctx, w, u, resp); err != nil {
return err
}
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
return err
}
default:
// We know the controller supports caching, but doesn't have a
// refresh token so clear out any refresh token we have for this resource.
if err := deleteRefreshToken(ctx, w, u, resourceType); err != nil {
return err
}
}
return nil
})
if err != nil {
return errors.Wrap(ctx, err, op)
}
if unsupportedCacheRequest {
return ErrRefreshNotSupported
}
return nil
}
+27 -10
View File
@@ -63,24 +63,17 @@ type SearchResult struct {
// cache.
type SearchService struct {
searchableResources map[SearchableResource]resourceSearcher
repo *Repository
}
// QueryAndLister defines the methods needed for listing an querying the
// resources supported by the boundary frontend.
type QueryAndLister interface {
QueryTargets(context.Context, string, string) ([]*targets.Target, error)
ListTargets(context.Context, string) ([]*targets.Target, error)
QuerySessions(context.Context, string, string) ([]*sessions.Session, error)
ListSessions(context.Context, string) ([]*sessions.Session, error)
}
func NewSearchService(ctx context.Context, repo QueryAndLister) (*SearchService, error) {
func NewSearchService(ctx context.Context, repo *Repository) (*SearchService, error) {
const op = "cache.NewSearchService"
switch {
case util.IsNil(repo):
return nil, errors.New(ctx, errors.InvalidParameter, op, "repo is nil")
}
return &SearchService{
repo: repo,
searchableResources: map[SearchableResource]resourceSearcher{
Targets: &resourceSearchFns[*targets.Target]{
list: repo.ListTargets,
@@ -100,6 +93,30 @@ func NewSearchService(ctx context.Context, repo QueryAndLister) (*SearchService,
}, nil
}
// Supported returns true if the provided search is supported for the provided
// auth token.
func (s *SearchService) Supported(ctx context.Context, t *AuthToken) (bool, error) {
const op = "cache.(SearchService).Supported"
switch {
case util.IsNil(t):
return false, errors.New(ctx, errors.InvalidParameter, op, "auth token is nil")
case t.UserId == "":
return false, errors.New(ctx, errors.InvalidParameter, op, "auth token's user id is empty")
}
u, err := s.repo.lookupUser(ctx, t.UserId)
if err != nil {
return false, errors.Wrap(ctx, err, op)
}
if u == nil {
return false, errors.New(ctx, errors.NotFound, op, "user not found for auth token")
}
cs, err := s.repo.cacheSupportState(ctx, u)
if err != nil {
return false, errors.Wrap(ctx, err, op)
}
return cs.supported != NotSupportedCacheSupport, nil
}
// Search returns a SearchResult based on the provided SearchParams. If the
// SearchParams doesn't have a valid searchable resource or an auth token id
// an error is returned. If the auth token id is unrecognized or is associated
+85
View File
@@ -7,6 +7,7 @@ import (
"context"
"sync"
"testing"
"time"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
@@ -103,6 +104,90 @@ func TestSearch_Errors(t *testing.T) {
}
}
func TestSupported(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
r, err := NewRepository(ctx, s, &sync.Map{},
mapBasedAuthTokenKeyringLookup(nil),
sliceBasedAuthTokenBoundaryReader(nil))
assert.NoError(t, err)
ss, err := NewSearchService(ctx, r)
require.NoError(t, err)
require.NotNil(t, ss)
t.Run("supported", func(t *testing.T) {
at := &AuthToken{
Id: "supported",
UserId: "supported",
}
{
rw := db.New(s)
u := &user{Id: at.UserId, Address: "address"}
require.NoError(t, rw.Create(ctx, u))
require.NoError(t, rw.Create(ctx, at))
refTok := &refreshToken{
UserId: at.Id,
ResourceType: targetResourceType,
RefreshToken: "sometherefreshtoken",
UpdateTime: time.Now(),
CreateTime: time.Now(),
}
require.NoError(t, rw.Create(ctx, refTok))
}
got, err := ss.Supported(ctx, at)
assert.NoError(t, err)
assert.True(t, got)
})
t.Run("unsupported", func(t *testing.T) {
at := &AuthToken{
Id: "unsupported",
UserId: "unsupported",
}
{
rw := db.New(s)
u := &user{Id: at.UserId, Address: "address"}
require.NoError(t, rw.Create(ctx, u))
require.NoError(t, rw.Create(ctx, at))
refTok := &refreshToken{
UserId: at.Id,
ResourceType: targetResourceType,
RefreshToken: sentinelNoRefreshToken,
UpdateTime: time.Now(),
CreateTime: time.Now(),
}
require.NoError(t, rw.Create(ctx, refTok))
}
got, err := ss.Supported(ctx, at)
assert.NoError(t, err)
assert.False(t, got)
})
t.Run("unkonwn", func(t *testing.T) {
at := &AuthToken{
Id: "unknown",
UserId: "unknown",
}
{
rw := db.New(s)
u := &user{Id: at.UserId, Address: "address"}
require.NoError(t, rw.Create(ctx, u))
require.NoError(t, rw.Create(ctx, at))
}
got, err := ss.Supported(ctx, at)
assert.NoError(t, err)
// No refresh token means we don't know if the boundary instance
// supports refresh tokens. Since we likely don't have any resources
// to search for anyways, default to saying it is supported.
assert.True(t, got)
})
}
func TestSearch(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
+39 -20
View File
@@ -41,12 +41,18 @@ type AuthTokenStatus struct {
KeyringlessReferences int
}
type BoundaryStatus struct {
// The boundary address for this user
Address string
CachingSupported CacheSupport
LastSupportCheck time.Duration
}
// UserStatus contains the status of a specific user tracked by the cache
type UserStatus struct {
// The Id of the user this status is for
Id string
// The boundary address for this user
Address string
Id string
BoundaryStatus BoundaryStatus
// The auth tokens used by this user to authenticate with the boundary instance
AuthTokens []AuthTokenStatus
// The resources tracked by the cache for this user
@@ -84,29 +90,39 @@ func (s *StatusService) Status(ctx context.Context) (*Status, error) {
}
for _, u := range users {
us := UserStatus{
Id: u.Id,
Address: u.Address,
Id: u.Id,
}
{
toks, err := s.repo.listTokens(ctx, u)
cacheSupported, err := s.repo.cacheSupportState(ctx, u)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
us.BoundaryStatus = BoundaryStatus{
Address: u.Address,
CachingSupported: cacheSupported.supported,
}
if cacheSupported.lastChecked != nil && !cacheSupported.lastChecked.IsZero() {
us.BoundaryStatus.LastSupportCheck = time.Since(*cacheSupported.lastChecked)
}
toks, err := s.repo.listTokens(ctx, u)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, t := range toks {
ts := &AuthTokenStatus{
Id: t.Id,
}
kt, err := s.repo.listKeyringTokens(ctx, t)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, t := range toks {
ts := &AuthTokenStatus{
Id: t.Id,
}
kt, err := s.repo.listKeyringTokens(ctx, t)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
ts.KeyringReferences = len(kt)
if _, ok := s.repo.idToKeyringlessAuthToken.Load(t.Id); ok {
ts.KeyringlessReferences = 1
}
us.AuthTokens = append(us.AuthTokens, *ts)
ts.KeyringReferences = len(kt)
if _, ok := s.repo.idToKeyringlessAuthToken.Load(t.Id); ok {
ts.KeyringlessReferences = 1
}
us.AuthTokens = append(us.AuthTokens, *ts)
}
for _, rt := range []resourceType{targetResourceType, sessionResourceType} {
@@ -213,6 +229,9 @@ func (s *StatusService) refreshTokenStatus(ctx context.Context, u *user, rt reso
}
return nil, errors.Wrap(ctx, err, op)
}
if refTok.RefreshToken == sentinelNoRefreshToken {
return nil, nil
}
ret := &RefreshTokenStatus{
LastUsed: time.Since(refTok.UpdateTime),
+79 -4
View File
@@ -103,8 +103,11 @@ func TestStatus(t *testing.T) {
assert.Equal(t, &Status{
Users: []UserStatus{
{
Id: u1.Id,
Address: u1.Address,
Id: u1.Id,
BoundaryStatus: BoundaryStatus{
Address: u1.Address,
CachingSupported: UnknownCacheSupport,
},
AuthTokens: []AuthTokenStatus{
{
Id: at1a.Id,
@@ -129,8 +132,11 @@ func TestStatus(t *testing.T) {
},
},
{
Id: u2.Id,
Address: u2.Address,
Id: u2.Id,
BoundaryStatus: BoundaryStatus{
Address: u2.Address,
CachingSupported: UnknownCacheSupport,
},
AuthTokens: []AuthTokenStatus{
{
Id: at2.Id,
@@ -252,3 +258,72 @@ func Map[T, U any](in []T, f func(T) U) []U {
}
return ret
}
func TestStatus_unsupported(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
boundaryAddr := "address"
u1 := &user{Id: "u1", Address: boundaryAddr}
at1 := &authtokens.AuthToken{
Id: "at_1a",
Token: "at_1a_token",
UserId: u1.Id,
ExpirationTime: time.Now().Add(time.Minute),
}
boundaryAuthTokens := []*authtokens.AuthToken{at1}
atMap := map[ringToken]*authtokens.AuthToken{
{k: "default", t: "default"}: at1,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
require.NoError(t, err)
ss, err := NewStatusService(ctx, r)
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
KeyringType: "default",
TokenName: "default",
AuthTokenId: at1.Id,
}))
err = r.refreshTargets(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
require.ErrorIs(t, err, ErrRefreshNotSupported)
got, err := ss.Status(ctx)
assert.NoError(t, err)
assert.Len(t, got.Users, 1)
assert.Greater(t, got.Users[0].BoundaryStatus.LastSupportCheck, time.Duration(0))
assert.LessOrEqual(t, got.Users[0].BoundaryStatus.LastSupportCheck, time.Second)
got.Users[0].BoundaryStatus.LastSupportCheck = 0
assert.Equal(t, got.Users, []UserStatus{
{
Id: u1.Id,
BoundaryStatus: BoundaryStatus{
Address: u1.Address,
CachingSupported: NotSupportedCacheSupport,
},
AuthTokens: []AuthTokenStatus{
{
Id: at1.Id,
KeyringReferences: 1,
},
},
Resources: []ResourceStatus{
{
Name: string(targetResourceType),
Count: 0,
},
{
Name: string(sessionResourceType),
Count: 0,
},
},
},
})
}