Refresh specific resources before searching when they are stale (#4046)
validate versions for all handlers. Refresh specific user before searching Pass flags on to the background process Allow the search refresh timeout to be configured
This commit is contained in:
1 parent
b3d268eae5
commit
cdc6fb0973
19 files changed
+543
-100
No files matched your search
+99
-2
@@ -6,6 +6,7 @@ package cache
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
@@ -16,15 +17,27 @@ import (
|
||||
|
||||
type RefreshService struct {
|
||||
repo *Repository
|
||||
// the amount of time that should have passed since the last refresh for a
|
||||
// call to RefreshForSearch will cause a refresh request to be sent to a
|
||||
// boundary controller.
|
||||
maxSearchStaleness time.Duration
|
||||
// the amount of time that RefreshForSearch should run before it times out.
|
||||
maxSearchRefreshTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewRefreshService(ctx context.Context, r *Repository) (*RefreshService, error) {
|
||||
func NewRefreshService(ctx context.Context, r *Repository, maxSearchStaleness, maxSearchRefreshTimeout time.Duration) (*RefreshService, error) {
|
||||
const op = "cache.NewRefreshService"
|
||||
switch {
|
||||
case util.IsNil(r):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "repository is nil")
|
||||
case maxSearchStaleness < 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "max search staleness is negative")
|
||||
}
|
||||
return &RefreshService{repo: r}, nil
|
||||
return &RefreshService{
|
||||
repo: r,
|
||||
maxSearchStaleness: maxSearchStaleness,
|
||||
maxSearchRefreshTimeout: maxSearchRefreshTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// cleanAndPickAuthTokens removes from the cache all auth tokens which are
|
||||
@@ -131,6 +144,90 @@ func (r *RefreshService) refreshableUsers(ctx context.Context, in []*user) ([]*u
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// RefreshForSearch refreshes a specific resource type owned by the user associated
|
||||
// with the provided auth token id, if it hasn't been refreshed in a certain
|
||||
// amount of time. If the resources has been updated recently, or if it is known
|
||||
// that the user's boundary instance doesn't support partial refreshing, this
|
||||
// method returns without any requests to the boundary controller.
|
||||
// While the criteria of whether the user will refresh or not is almost the same
|
||||
// as the criteria used in Refresh, RefreshForSearch will not refresh any
|
||||
// data if there is not a refresh token stored for the resource. It
|
||||
// might make sense to change this in the future, but the reasoning used is
|
||||
// that we should not be making an initial load of all resources while blocking
|
||||
// a search query, in case we have not yet even attempted to load the resources
|
||||
// for this user yet.
|
||||
// Note: Currently, if the context timesout we stop refreshing completely and
|
||||
// return to the caller. A possible enhancement in the future would be to return
|
||||
// when the context is Done, but allow the refresh to proceed in the background.
|
||||
func (r *RefreshService) RefreshForSearch(ctx context.Context, authTokenid string, resourceType SearchableResource, opt ...Option) error {
|
||||
const op = "cache.(RefreshService).RefreshForSearch"
|
||||
if r.maxSearchRefreshTimeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, r.maxSearchRefreshTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
at, err := r.repo.LookupToken(ctx, authTokenid)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if at == nil {
|
||||
return errors.New(ctx, errors.NotFound, op, "auth token not found")
|
||||
}
|
||||
u, err := r.repo.lookupUser(ctx, at.UserId)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if u == nil {
|
||||
return errors.New(ctx, errors.NotFound, op, "user not found")
|
||||
}
|
||||
us, err := r.refreshableUsers(ctx, []*user{u})
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
switch len(us) {
|
||||
case 0:
|
||||
// The provided user is not refreshable, so do not refresh.
|
||||
return nil
|
||||
case 1:
|
||||
u = us[0]
|
||||
default:
|
||||
return errors.New(ctx, errors.Internal, op, "unexpected number of refreshable users")
|
||||
}
|
||||
|
||||
tokens, err := r.cleanAndPickAuthTokens(ctx, u)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
switch resourceType {
|
||||
case Targets:
|
||||
rtv, err := r.repo.lookupRefreshToken(ctx, u, targetResourceType)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if rtv != nil && time.Since(rtv.UpdateTime) > r.maxSearchStaleness {
|
||||
if err := r.repo.refreshTargets(ctx, u, tokens, opt...); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
case Sessions:
|
||||
rtv, err := r.repo.lookupRefreshToken(ctx, u, sessionResourceType)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if rtv != nil && time.Since(rtv.UpdateTime) > r.maxSearchStaleness {
|
||||
if err := r.repo.refreshSessions(ctx, u, tokens, opt...); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "unrecognized resource type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh iterates over all tokens in the cache that are for users who either
|
||||
// have a refresh token or which do not have any resources in the cache yet. It
|
||||
// then attempts to read those user's resources from boundary and updates the
|
||||
|
||||
+172
-9
@@ -52,6 +52,8 @@ func testStaticResourceRetrievalFunc[T any](t *testing.T, ret [][]T, removed [][
|
||||
}
|
||||
}
|
||||
|
||||
// testFullFetchRetrievalFunc 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) {
|
||||
return func(_ context.Context, _, _ string, _ RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
||||
return ret, nil, "", nil
|
||||
@@ -137,7 +139,7 @@ func TestCleanAndPickTokens(t *testing.T) {
|
||||
mapBasedAuthTokenKeyringLookup(atMap),
|
||||
fakeBoundaryLookupFn)
|
||||
require.NoError(t, err)
|
||||
rs, err := NewRefreshService(ctx, r)
|
||||
rs, err := NewRefreshService(ctx, r, 0, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("unknown user", func(t *testing.T) {
|
||||
@@ -331,6 +333,167 @@ func TestCleanAndPickTokens(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshForSearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
boundaryAddr := "address"
|
||||
u := &user{Id: "u1", Address: boundaryAddr}
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: u.Id,
|
||||
ExpirationTime: time.Now().Add(time.Minute),
|
||||
}
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
||||
atMap := make(map[ringToken]*authtokens.AuthToken)
|
||||
|
||||
atMap[ringToken{"k", "t"}] = at
|
||||
|
||||
t.Run("targets refreshed for searching", func(t *testing.T) {
|
||||
s, err := db.Open(ctx)
|
||||
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)
|
||||
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"),
|
||||
}
|
||||
opts := []Option{
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t,
|
||||
[][]*targets.Target{
|
||||
retTargets[:3],
|
||||
retTargets[3:],
|
||||
},
|
||||
[][]string{
|
||||
nil,
|
||||
{retTargets[0].Id, retTargets[1].Id},
|
||||
},
|
||||
)),
|
||||
}
|
||||
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Targets, opts...))
|
||||
|
||||
cachedTargets, err := r.ListTargets(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, cachedTargets)
|
||||
|
||||
// Now load up a few resources and a token, and trying again should
|
||||
// see the RefreshForSearch update more fields.
|
||||
assert.NoError(t, rs.Refresh(ctx, opts...))
|
||||
cachedTargets, err = r.ListTargets(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retTargets[:3], cachedTargets)
|
||||
|
||||
// Let 2 milliseconds pass so the items are stale enough
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Targets, opts...))
|
||||
cachedTargets, err = r.ListTargets(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retTargets[2:], cachedTargets)
|
||||
})
|
||||
|
||||
t.Run("no refresh token no refresh for search", func(t *testing.T) {
|
||||
s, err := db.Open(ctx)
|
||||
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)
|
||||
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]))))
|
||||
|
||||
got, err := r.ListTargets(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retTargets[:2], got)
|
||||
|
||||
// Let 2 milliseconds pass so the items are stale enough
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
t.Run("sessions refreshed for searching", func(t *testing.T) {
|
||||
s, err := db.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
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}))
|
||||
|
||||
retSess := []*sessions.Session{
|
||||
session("1"),
|
||||
session("2"),
|
||||
session("3"),
|
||||
session("4"),
|
||||
}
|
||||
opts := []Option{
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t,
|
||||
[][]*sessions.Session{
|
||||
retSess[:3],
|
||||
retSess[3:],
|
||||
},
|
||||
[][]string{
|
||||
nil,
|
||||
{retSess[0].Id, retSess[1].Id},
|
||||
},
|
||||
)),
|
||||
}
|
||||
|
||||
// First call doesn't sync anything because no sessions were already synced yet
|
||||
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Sessions, opts...))
|
||||
cachedSessions, err := r.ListSessions(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, cachedSessions)
|
||||
|
||||
assert.NoError(t, rs.Refresh(ctx, opts...))
|
||||
cachedSessions, err = r.ListSessions(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retSess[:3], cachedSessions)
|
||||
|
||||
// Second call removes the first 2 resources from the cache and adds the last
|
||||
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Sessions, opts...))
|
||||
cachedSessions, err = r.ListSessions(ctx, at.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, retSess[2:], cachedSessions)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -353,7 +516,7 @@ func TestRefresh(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)
|
||||
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}))
|
||||
|
||||
@@ -394,7 +557,7 @@ func TestRefresh(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)
|
||||
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}))
|
||||
|
||||
@@ -434,7 +597,7 @@ func TestRefresh(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)
|
||||
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}))
|
||||
|
||||
@@ -462,7 +625,7 @@ func TestRefresh(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)
|
||||
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}))
|
||||
|
||||
@@ -516,7 +679,7 @@ func TestFullFetch(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)
|
||||
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}))
|
||||
|
||||
@@ -559,7 +722,7 @@ func TestFullFetch(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)
|
||||
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}))
|
||||
|
||||
@@ -597,7 +760,7 @@ func TestFullFetch(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)
|
||||
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}))
|
||||
|
||||
@@ -630,7 +793,7 @@ func TestFullFetch(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)
|
||||
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}))
|
||||
|
||||
|
||||
+6
-3
@@ -26,6 +26,7 @@ type KeyringTokenLookupFn func(keyring string, tokenName string) *authtokens.Aut
|
||||
type BoundaryTokenReaderFn func(ctx context.Context, addr string, authToken string) (*authtokens.AuthToken, error)
|
||||
|
||||
type Repository struct {
|
||||
serverCtx context.Context
|
||||
rw *db.Db
|
||||
tokenKeyringFn KeyringTokenLookupFn
|
||||
tokenReadFromBoundaryFn BoundaryTokenReaderFn
|
||||
@@ -33,7 +34,8 @@ type Repository struct {
|
||||
idToKeyringlessAuthToken *sync.Map
|
||||
}
|
||||
|
||||
// NewRepository returns a cache repository. The provided Store must be
|
||||
// NewRepository returns a cache repository. The provided context is stored as
|
||||
// the server context for purposes like storing boundary request errors.
|
||||
func NewRepository(ctx context.Context, conn *db.DB, idToAuthToken *sync.Map, keyringFn KeyringTokenLookupFn, atReadFn BoundaryTokenReaderFn, opt ...Option) (*Repository, error) {
|
||||
const op = "cache.NewRepository"
|
||||
switch {
|
||||
@@ -47,6 +49,7 @@ func NewRepository(ctx context.Context, conn *db.DB, idToAuthToken *sync.Map, ke
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token read function")
|
||||
}
|
||||
return &Repository{
|
||||
serverCtx: ctx,
|
||||
rw: db.New(conn),
|
||||
tokenKeyringFn: keyringFn,
|
||||
tokenReadFromBoundaryFn: atReadFn,
|
||||
@@ -110,8 +113,8 @@ func (r *Repository) lookupError(ctx context.Context, u *user, resourceType reso
|
||||
type apiError struct {
|
||||
UserId string `gorm:"primaryKey"`
|
||||
ResourceType resourceType `gorm:"primaryKey"`
|
||||
Error string
|
||||
CreateTime time.Time
|
||||
Error string `gorm:"default:null"`
|
||||
CreateTime time.Time `gorm:"default:(strftime('%Y-%m-%d %H:%M:%f','now'))"`
|
||||
}
|
||||
|
||||
func (*apiError) TableName() string {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
// RefreshTokenValue is the the type for the actual refresh token value handled
|
||||
@@ -21,15 +20,15 @@ type RefreshTokenValue string
|
||||
// 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.
|
||||
func (r *Repository) lookupRefreshToken(ctx context.Context, u *user, resourceType resourceType) (RefreshTokenValue, error) {
|
||||
func (r *Repository) lookupRefreshToken(ctx context.Context, u *user, resourceType resourceType) (*refreshToken, error) {
|
||||
const op = "cache.(Repsoitory).lookupRefreshToken"
|
||||
switch {
|
||||
case util.IsNil(u):
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
case !resourceType.valid():
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
}
|
||||
|
||||
rt := &refreshToken{
|
||||
@@ -37,12 +36,12 @@ func (r *Repository) lookupRefreshToken(ctx context.Context, u *user, resourceTy
|
||||
ResourceType: resourceType,
|
||||
}
|
||||
if err := r.rw.LookupById(ctx, rt); err != nil {
|
||||
if errors.Is(err, dbw.ErrRecordNotFound) {
|
||||
return "", nil
|
||||
if errors.IsNotFoundError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return "", errors.Wrap(ctx, err, op)
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return rt.RefreshToken, nil
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
// listRefreshTokens returns all refresh tokens associated with a specific user
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
@@ -119,6 +120,7 @@ func TestLookupRefreshToken(t *testing.T) {
|
||||
known := &user{Id: "withrefreshtoken", Address: "addr"}
|
||||
require.NoError(t, r.rw.Create(ctx, known))
|
||||
|
||||
before := time.Now().Truncate(time.Millisecond).UTC()
|
||||
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
|
||||
@@ -126,7 +128,8 @@ func TestLookupRefreshToken(t *testing.T) {
|
||||
|
||||
got, err := r.lookupRefreshToken(ctx, known, targetResourceType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, token, got)
|
||||
assert.Equal(t, token, got.RefreshToken)
|
||||
assert.GreaterOrEqual(t, got.CreateTime, before)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -65,10 +65,14 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
if opts.withSessionRetrievalFunc == nil {
|
||||
opts.withSessionRetrievalFunc = defaultSessionFunc
|
||||
}
|
||||
var oldRefreshTokenVal RefreshTokenValue
|
||||
oldRefreshToken, err := r.lookupRefreshToken(ctx, u, resourceType)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if oldRefreshToken != nil {
|
||||
oldRefreshTokenVal = oldRefreshToken.RefreshToken
|
||||
}
|
||||
|
||||
// Find and use a token for retrieving sessions
|
||||
var gotResponse bool
|
||||
@@ -77,13 +81,13 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
var removedIds []string
|
||||
var retErr error
|
||||
for at, t := range tokens {
|
||||
resp, removedIds, newRefreshToken, err = opts.withSessionRetrievalFunc(ctx, u.Address, t, oldRefreshToken)
|
||||
resp, removedIds, newRefreshToken, err = opts.withSessionRetrievalFunc(ctx, u.Address, t, oldRefreshTokenVal)
|
||||
if api.ErrInvalidRefreshToken.Is(err) {
|
||||
if err := r.deleteRefreshToken(ctx, u, resourceType); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// try again without the refresh token
|
||||
oldRefreshToken = ""
|
||||
oldRefreshToken = nil
|
||||
resp, removedIds, newRefreshToken, err = opts.withSessionRetrievalFunc(ctx, u.Address, t, "")
|
||||
}
|
||||
if err != nil {
|
||||
@@ -95,7 +99,7 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
}
|
||||
|
||||
if retErr != nil {
|
||||
if saveErr := r.saveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
if saveErr := r.saveError(r.serverCtx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
@@ -106,7 +110,7 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
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 {
|
||||
switch {
|
||||
case oldRefreshToken == "":
|
||||
case oldRefreshToken == nil:
|
||||
if _, err := w.Exec(ctx, "delete from session where user_id = @user_id",
|
||||
[]any{sql.Named("user_id", u.Id)}); err != nil {
|
||||
return err
|
||||
@@ -171,7 +175,7 @@ func (r *Repository) fullFetchSessions(ctx context.Context, u *user, tokens map[
|
||||
}
|
||||
|
||||
if retErr != nil {
|
||||
if saveErr := r.saveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
if saveErr := r.saveError(r.serverCtx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
|
||||
+14
-9
@@ -63,26 +63,31 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
if opts.withTargetRetrievalFunc == nil {
|
||||
opts.withTargetRetrievalFunc = defaultTargetFunc
|
||||
}
|
||||
|
||||
var oldRefreshTokenVal RefreshTokenValue
|
||||
oldRefreshToken, err := r.lookupRefreshToken(ctx, u, resourceType)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if oldRefreshToken != nil {
|
||||
oldRefreshTokenVal = oldRefreshToken.RefreshToken
|
||||
}
|
||||
|
||||
// Find and use a token for retrieving targets
|
||||
var gotResponse bool
|
||||
var resp []*targets.Target
|
||||
var removedIds []string
|
||||
var newRefreshToken RefreshTokenValue
|
||||
var newRefreshTokenVal RefreshTokenValue
|
||||
var retErr error
|
||||
for at, t := range tokens {
|
||||
resp, removedIds, newRefreshToken, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, oldRefreshToken)
|
||||
resp, removedIds, newRefreshTokenVal, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, oldRefreshTokenVal)
|
||||
if api.ErrInvalidRefreshToken.Is(err) {
|
||||
if err := r.deleteRefreshToken(ctx, u, resourceType); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// try again without the refresh token
|
||||
oldRefreshToken = ""
|
||||
resp, removedIds, newRefreshToken, err = opts.withTargetRetrievalFunc(ctx, u.Address, t, "")
|
||||
oldRefreshToken = nil
|
||||
resp, removedIds, newRefreshTokenVal, 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)))
|
||||
@@ -92,7 +97,7 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
break
|
||||
}
|
||||
if retErr != nil {
|
||||
if saveErr := r.saveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
if saveErr := r.saveError(r.serverCtx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
@@ -103,7 +108,7 @@ 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 {
|
||||
switch {
|
||||
case oldRefreshToken == "":
|
||||
case oldRefreshToken == nil:
|
||||
if _, err := w.Exec(ctx, "delete from target where user_id = @user_id",
|
||||
[]any{sql.Named("user_id", u.Id)}); err != nil {
|
||||
return err
|
||||
@@ -118,8 +123,8 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
if err := upsertTargets(ctx, w, u, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
if newRefreshToken != "" || oldRefreshToken != "" {
|
||||
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshToken); err != nil {
|
||||
if newRefreshTokenVal != "" || oldRefreshToken != nil {
|
||||
if err := upsertRefreshToken(ctx, w, u, resourceType, newRefreshTokenVal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -167,7 +172,7 @@ func (r *Repository) fullFetchTargets(ctx context.Context, u *user, tokens map[A
|
||||
break
|
||||
}
|
||||
if retErr != nil {
|
||||
if saveErr := r.saveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
if saveErr := r.saveError(r.serverCtx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,6 +382,23 @@ func cleanExpiredOrOrphanedAuthTokens(ctx context.Context, writer db.Writer, idT
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupUser returns a user if one is present in the repository or nil if not.
|
||||
func (r *Repository) lookupUser(ctx context.Context, id string) (*user, error) {
|
||||
const op = "cache.(Repository).lookupUser"
|
||||
switch {
|
||||
case id == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "empty id")
|
||||
}
|
||||
ret := &user{Id: id}
|
||||
if err := r.rw.LookupById(ctx, ret); err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// listUsers returns all known tokens in the cache
|
||||
func (r *Repository) listUsers(ctx context.Context) ([]*user, error) {
|
||||
const op = "cache.(Repository).listUsers"
|
||||
|
||||
@@ -610,6 +610,48 @@ func TestRepository_LookupToken(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_lookupUpser(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: "u_1",
|
||||
}
|
||||
kt := KeyringToken{
|
||||
TokenName: "t1",
|
||||
KeyringType: "k1",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{kt.KeyringType, kt.TokenName}: at,
|
||||
}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
||||
|
||||
t.Run("empty user id", func(t *testing.T) {
|
||||
u, err := r.lookupUser(ctx, "")
|
||||
assert.ErrorContains(t, err, "empty id")
|
||||
assert.Nil(t, u)
|
||||
})
|
||||
t.Run("not found user id", func(t *testing.T) {
|
||||
u, err := r.lookupUser(ctx, "notfound")
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, u)
|
||||
})
|
||||
t.Run("found", func(t *testing.T) {
|
||||
u, err := r.lookupUser(ctx, at.UserId)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &user{Id: at.UserId, Address: addr}, u)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_RemoveStaleTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
|
||||
Reference in new issue
Block a user