mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1478 lines
55 KiB
1478 lines
55 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/api"
|
|
"github.com/hashicorp/boundary/api/aliases"
|
|
"github.com/hashicorp/boundary/api/authtokens"
|
|
"github.com/hashicorp/boundary/api/sessions"
|
|
"github.com/hashicorp/boundary/api/targets"
|
|
"github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
// testStaticResourceRetrievalFunc returns a function that always returns the
|
|
// provided slice and a nil error. The returned function can be passed into the
|
|
// options that provide a resource retrieval func such as
|
|
// WithTargetRetrievalFunc and WithSessionRetrievalFunc. The provided refresh
|
|
// token determines the returned value and is a string representation of an
|
|
// incrementing integer. This integer is the index into the provided return
|
|
// values and once it reaches the length of the provided slice it returns an
|
|
// empty slice and the same refresh token repeatedly.
|
|
func testStaticResourceRetrievalFunc[T any](t *testing.T, ret [][]T, removed [][]string) func(context.Context, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
t.Helper()
|
|
require.Equal(t, len(ret), len(removed), "returned slice and removed slice must be the same length")
|
|
return func(ctx context.Context, s1, s2 string, refToken RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
index := 0
|
|
if refToken != "" {
|
|
var err error
|
|
index, err = strconv.Atoi(string(refToken))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
switch {
|
|
case len(ret) == 0:
|
|
return nil, nil, "", nil
|
|
case index > 0 && index >= len(ret):
|
|
return []T{}, []string{}, RefreshTokenValue(fmt.Sprintf("%d", index)), nil
|
|
default:
|
|
return ret[index], removed[index], RefreshTokenValue(fmt.Sprintf("%d", index+1)), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// testNoRefreshRetrievalFunc simulates a controller that doesn't support refresh
|
|
// since it does not return any refresh token.
|
|
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 nil, nil, "", ErrRefreshNotSupported
|
|
}
|
|
}
|
|
|
|
// testErroringForRefreshTokenRetrievalFunc returns a refresh token error when
|
|
// the refresh token is not empty. This is useful for testing behavior when
|
|
// the refresh token has expired or is otherwise invalid.
|
|
func testErroringForRefreshTokenRetrievalFunc[T any](t *testing.T, ret []T) func(context.Context, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
return func(ctx context.Context, s1, s2 string, refToken RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
if refToken != "" {
|
|
return nil, nil, "", api.ErrInvalidListToken
|
|
}
|
|
return ret, nil, "1", nil
|
|
}
|
|
}
|
|
|
|
// testStaticResourceRetrievalFunc returns a function that always returns the
|
|
// provided slice and a nil error. The returned function can be passed into the
|
|
// options that provide a resource retrieval func such as
|
|
// WithTargetRetrievalFunc and WithSessionRetrievalFunc. The provided refresh
|
|
// token determines the returned value and is a string representation of an
|
|
// incrementing integer. This integer is the index into the provided return
|
|
// values and once it reaches the length of the provided slice it returns an
|
|
// empty slice and the same refresh token repeatedly. This is for retrieval
|
|
// functions that require an id be provided for listing purposes like when
|
|
// listing resolvable aliases.
|
|
func testStaticResourceRetrievalFuncForId[T any](t *testing.T, ret [][]T, removed [][]string) func(context.Context, string, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
t.Helper()
|
|
require.Equal(t, len(ret), len(removed), "returned slice and removed slice must be the same length")
|
|
return func(ctx context.Context, s1, s2, s3 string, refToken RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
index := 0
|
|
if refToken != "" {
|
|
var err error
|
|
index, err = strconv.Atoi(string(refToken))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
switch {
|
|
case len(ret) == 0:
|
|
return nil, nil, "", nil
|
|
case index > 0 && index >= len(ret):
|
|
return []T{}, []string{}, RefreshTokenValue(fmt.Sprintf("%d", index)), nil
|
|
default:
|
|
return ret[index], removed[index], RefreshTokenValue(fmt.Sprintf("%d", index+1)), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// testNoRefreshRetrievalFunc simulates a controller that doesn't support refresh
|
|
// since it does not return any refresh token. This is for retrieval
|
|
// functions that require an id be provided for listing purposes like when
|
|
// listing resolvable aliases.
|
|
func testNoRefreshRetrievalFuncForId[T any](t *testing.T) func(context.Context, string, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
return func(_ context.Context, _, _, _ string, _ RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
return nil, nil, "", ErrRefreshNotSupported
|
|
}
|
|
}
|
|
|
|
// testErroringForRefreshTokenRetrievalFuncForId returns a refresh token error when
|
|
// the refresh token is not empty. This is useful for testing behavior when
|
|
// the refresh token has expired or is otherwise invalid. This is for retrieval
|
|
// functions that require an id be provided for listing purposes like when
|
|
// listing resolvable aliases.
|
|
func testErroringForRefreshTokenRetrievalFuncForId[T any](t *testing.T, ret []T) func(context.Context, string, string, string, RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
return func(ctx context.Context, s1, s2, s3 string, refToken RefreshTokenValue) ([]T, []string, RefreshTokenValue, error) {
|
|
if refToken != "" {
|
|
return nil, nil, "", api.ErrInvalidListToken
|
|
}
|
|
return ret, nil, "1", nil
|
|
}
|
|
}
|
|
|
|
func TestCleanAndPickTokens(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := db.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
boundaryAddr := "address"
|
|
u1 := &user{Id: "u1", Address: boundaryAddr}
|
|
at1a := &authtokens.AuthToken{
|
|
Id: "at_1a",
|
|
Token: "at_1a_token",
|
|
UserId: u1.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
at1b := &authtokens.AuthToken{
|
|
Id: "at_1b",
|
|
Token: "at_1b_token",
|
|
UserId: u1.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
|
|
keyringOnlyUser := &user{Id: "keyringUser", Address: boundaryAddr}
|
|
keyringAuthToken1 := &authtokens.AuthToken{
|
|
Id: "at_2a",
|
|
Token: "at_2a_token",
|
|
UserId: keyringOnlyUser.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
keyringAuthToken2 := &authtokens.AuthToken{
|
|
Id: "at_2b",
|
|
Token: "at_2b_token",
|
|
UserId: keyringOnlyUser.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at1a, keyringAuthToken1, at1b, keyringAuthToken2}
|
|
unauthorizedAuthTokens := []*authtokens.AuthToken{}
|
|
notFoundAuthTokens := []*authtokens.AuthToken{}
|
|
randomErrorAuthTokens := []*authtokens.AuthToken{}
|
|
fakeBoundaryLookupFn := func(ctx context.Context, addr, at string) (*authtokens.AuthToken, error) {
|
|
for _, v := range randomErrorAuthTokens {
|
|
if at == v.Token {
|
|
return nil, errors.New("test error")
|
|
}
|
|
}
|
|
for _, v := range notFoundAuthTokens {
|
|
if at == v.Token {
|
|
return nil, api.ErrNotFound
|
|
}
|
|
}
|
|
for _, v := range unauthorizedAuthTokens {
|
|
if at == v.Token {
|
|
return nil, api.ErrUnauthorized
|
|
}
|
|
}
|
|
for _, v := range boundaryAuthTokens {
|
|
if at == v.Token {
|
|
return v, nil
|
|
}
|
|
}
|
|
return nil, errors.New("not found")
|
|
}
|
|
|
|
atMap := make(map[ringToken]*authtokens.AuthToken)
|
|
r, err := NewRepository(ctx, s, &sync.Map{},
|
|
mapBasedAuthTokenKeyringLookup(atMap),
|
|
fakeBoundaryLookupFn)
|
|
require.NoError(t, err)
|
|
rs, err := NewRefreshService(ctx, r, hclog.NewNullLogger(), 0, 0)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("unknown user", func(t *testing.T) {
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, &user{Id: "unknownuser", Address: "unknown"})
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, got)
|
|
})
|
|
|
|
t.Run("both memory and keyring stored token", func(t *testing.T) {
|
|
key := ringToken{"k1", "t1"}
|
|
atMap[key] = at1a
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key.k,
|
|
TokenName: key.t,
|
|
AuthTokenId: at1a.Id,
|
|
}))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
|
|
// delete the keyringToken from the keyring and see it get removed from the response
|
|
delete(atMap, key)
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1b.Token})
|
|
})
|
|
|
|
t.Run("2 memory tokens", func(t *testing.T) {
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1a.Token))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
})
|
|
|
|
t.Run("boundary in memory auth token expires", func(t *testing.T) {
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1a.Token))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
|
|
t.Cleanup(func() {
|
|
unauthorizedAuthTokens = nil
|
|
})
|
|
|
|
unauthorizedAuthTokens = []*authtokens.AuthToken{at1b}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token})
|
|
})
|
|
|
|
t.Run("boundary in memory auth token not found", func(t *testing.T) {
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1a.Token))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
|
|
t.Cleanup(func() {
|
|
notFoundAuthTokens = nil
|
|
})
|
|
|
|
notFoundAuthTokens = []*authtokens.AuthToken{at1b}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token})
|
|
})
|
|
|
|
t.Run("boundary keyring auths token expires", func(t *testing.T) {
|
|
key1 := ringToken{"k1", "t1"}
|
|
atMap[key1] = keyringAuthToken1
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key1.k,
|
|
TokenName: key1.t,
|
|
AuthTokenId: keyringAuthToken1.Id,
|
|
}))
|
|
key2 := ringToken{"k2", "t2"}
|
|
atMap[key2] = keyringAuthToken2
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key2.k,
|
|
TokenName: key2.t,
|
|
AuthTokenId: keyringAuthToken2.Id,
|
|
}))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token, keyringAuthToken2.Token})
|
|
|
|
t.Cleanup(func() {
|
|
unauthorizedAuthTokens = nil
|
|
})
|
|
|
|
unauthorizedAuthTokens = []*authtokens.AuthToken{keyringAuthToken2}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token})
|
|
})
|
|
|
|
t.Run("boundary in memory auth token check errors", func(t *testing.T) {
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1a.Token))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
|
|
t.Cleanup(func() {
|
|
randomErrorAuthTokens = nil
|
|
})
|
|
|
|
randomErrorAuthTokens = []*authtokens.AuthToken{at1b}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token})
|
|
})
|
|
|
|
t.Run("boundary keyring auths token check errors", func(t *testing.T) {
|
|
key1 := ringToken{"k1", "t1"}
|
|
atMap[key1] = keyringAuthToken1
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key1.k,
|
|
TokenName: key1.t,
|
|
AuthTokenId: keyringAuthToken1.Id,
|
|
}))
|
|
key2 := ringToken{"k2", "t2"}
|
|
atMap[key2] = keyringAuthToken2
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key2.k,
|
|
TokenName: key2.t,
|
|
AuthTokenId: keyringAuthToken2.Id,
|
|
}))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token, keyringAuthToken2.Token})
|
|
|
|
t.Cleanup(func() {
|
|
randomErrorAuthTokens = nil
|
|
})
|
|
|
|
randomErrorAuthTokens = []*authtokens.AuthToken{keyringAuthToken2}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token})
|
|
})
|
|
|
|
t.Run("2 keyring tokens", func(t *testing.T) {
|
|
key1 := ringToken{"k1", "t1"}
|
|
atMap[key1] = keyringAuthToken1
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key1.k,
|
|
TokenName: key1.t,
|
|
AuthTokenId: keyringAuthToken1.Id,
|
|
}))
|
|
key2 := ringToken{"k2", "t2"}
|
|
atMap[key2] = keyringAuthToken2
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key2.k,
|
|
TokenName: key2.t,
|
|
AuthTokenId: keyringAuthToken2.Id,
|
|
}))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token, keyringAuthToken2.Token})
|
|
|
|
// Removing all keyring references and then cleaning auth tokens
|
|
// removes all auth tokens, along with the user
|
|
gotU, err := r.listUsers(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, gotU, keyringOnlyUser)
|
|
|
|
delete(atMap, key1)
|
|
delete(atMap, key2)
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, got)
|
|
|
|
gotT, err := r.listTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, gotT)
|
|
gotU, err = r.listUsers(ctx)
|
|
assert.NoError(t, err)
|
|
assert.NotContains(t, gotU, keyringOnlyUser)
|
|
})
|
|
}
|
|
|
|
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, hclog.NewNullLogger(), 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{
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
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)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, cachedTargets.Targets)
|
|
assert.Empty(t, cachedTargets.ResolvableAliases)
|
|
assert.Empty(t, cachedTargets.Sessions)
|
|
assert.False(t, cachedTargets.Incomplete)
|
|
|
|
// 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.Targets)
|
|
|
|
// 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.Targets)
|
|
})
|
|
|
|
t.Run("targets forced 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)
|
|
// Everything can stay stale for an hour
|
|
rs, err := NewRefreshService(ctx, r, hclog.NewNullLogger(), time.Hour, 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{
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
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)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, cachedTargets.Targets)
|
|
assert.Empty(t, cachedTargets.ResolvableAliases)
|
|
assert.Empty(t, cachedTargets.Sessions)
|
|
assert.False(t, cachedTargets.Incomplete)
|
|
|
|
// 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.Targets)
|
|
|
|
// No refresh happened because it is not considered stale
|
|
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[:3], cachedTargets.Targets)
|
|
|
|
// Now force refresh
|
|
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Targets, append(opts, WithIgnoreSearchStaleness(true))...))
|
|
cachedTargets, err = r.ListTargets(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retTargets[2:], cachedTargets.Targets)
|
|
})
|
|
|
|
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, hclog.NewNullLogger(), 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"),
|
|
}
|
|
|
|
// Get the first set of resources, but no refresh tokens
|
|
err = rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
|
|
assert.ErrorContains(t, err, ErrRefreshNotSupported.Error())
|
|
|
|
got, err := r.ListTargets(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
|
|
// 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,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
|
|
assert.Nil(t, err)
|
|
|
|
err = rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
|
|
assert.Nil(t, err)
|
|
|
|
got, err = r.ListTargets(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
|
|
// Now simulate the controller updating to support refresh tokens and
|
|
// the resources starting to be cached.
|
|
err = rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, [][]*targets.Target{retTargets}, [][]string{{}})))
|
|
assert.Nil(t, err, err)
|
|
|
|
got, err = r.ListTargets(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, got.Targets, 2)
|
|
})
|
|
|
|
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, hclog.NewNullLogger(), 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{
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
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)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, cachedSessions.Targets)
|
|
assert.Empty(t, cachedSessions.ResolvableAliases)
|
|
assert.Empty(t, cachedSessions.Sessions)
|
|
assert.False(t, cachedSessions.Incomplete)
|
|
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedSessions, err = r.ListSessions(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retSess[:3], cachedSessions.Sessions)
|
|
|
|
// 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.Sessions)
|
|
})
|
|
|
|
t.Run("sessions forced 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)
|
|
// Everything stays fresh for 1 hour
|
|
rs, err := NewRefreshService(ctx, r, hclog.NewNullLogger(), time.Hour, 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{
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
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)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, cachedSessions.Targets)
|
|
assert.Empty(t, cachedSessions.ResolvableAliases)
|
|
assert.Empty(t, cachedSessions.Sessions)
|
|
assert.False(t, cachedSessions.Incomplete)
|
|
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedSessions, err = r.ListSessions(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retSess[:3], cachedSessions.Sessions)
|
|
|
|
// Refresh for search doesn't refresh anything because it isn't stale
|
|
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[:3], cachedSessions.Sessions)
|
|
|
|
// Now force the refresh and see things get updated
|
|
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Sessions, append(opts, WithIgnoreSearchStaleness(true))...))
|
|
cachedSessions, err = r.ListSessions(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retSess[2:], cachedSessions.Sessions)
|
|
})
|
|
|
|
t.Run("aliases 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, hclog.Default(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
retAl := []*aliases.Alias{
|
|
alias("1"),
|
|
alias("2"),
|
|
alias("3"),
|
|
alias("4"),
|
|
}
|
|
opts := []Option{
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t,
|
|
[][]*aliases.Alias{
|
|
retAl[:3],
|
|
retAl[3:],
|
|
},
|
|
[][]string{
|
|
nil,
|
|
{retAl[0].Id, retAl[1].Id},
|
|
},
|
|
)),
|
|
}
|
|
|
|
// First call doesn't sync anything because no aliases were already synced yet
|
|
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, ResolvableAliases, opts...))
|
|
cachedAliases, err := r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, cachedAliases.Targets)
|
|
assert.Empty(t, cachedAliases.ResolvableAliases)
|
|
assert.Empty(t, cachedAliases.Sessions)
|
|
assert.False(t, cachedAliases.Incomplete)
|
|
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedAliases, err = r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, retAl[:3], cachedAliases.ResolvableAliases)
|
|
|
|
// Second call removes the first 2 resources from the cache and adds the last
|
|
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, ResolvableAliases, opts...))
|
|
cachedAliases, err = r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, retAl[2:], cachedAliases.ResolvableAliases)
|
|
})
|
|
|
|
t.Run("aliases forced 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)
|
|
// Everything stays fresh for 1 hour
|
|
rs, err := NewRefreshService(ctx, r, hclog.Default(), time.Hour, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
retAls := []*aliases.Alias{
|
|
alias("1"),
|
|
alias("2"),
|
|
alias("3"),
|
|
alias("4"),
|
|
}
|
|
opts := []Option{
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t,
|
|
[][]*aliases.Alias{
|
|
retAls[:3],
|
|
retAls[3:],
|
|
},
|
|
[][]string{
|
|
nil,
|
|
{retAls[0].Id, retAls[1].Id},
|
|
},
|
|
)),
|
|
}
|
|
|
|
// First call doesn't sync anything because no aliases were already synced yet
|
|
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, ResolvableAliases, opts...))
|
|
cachedAliases, err := r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, cachedAliases.Targets)
|
|
assert.Empty(t, cachedAliases.ResolvableAliases)
|
|
assert.Empty(t, cachedAliases.Sessions)
|
|
assert.False(t, cachedAliases.Incomplete)
|
|
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedAliases, err = r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, retAls[:3], cachedAliases.ResolvableAliases)
|
|
|
|
// Refresh for search doesn't refresh anything because it isn't stale
|
|
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, ResolvableAliases, opts...))
|
|
cachedAliases, err = r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, retAls[:3], cachedAliases.ResolvableAliases)
|
|
|
|
// Now force the refresh and see things get updated
|
|
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, ResolvableAliases, append(opts, WithIgnoreSearchStaleness(true))...))
|
|
cachedAliases, err = r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, retAls[2:], cachedAliases.ResolvableAliases)
|
|
})
|
|
}
|
|
|
|
func TestRefreshNonBlocking(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) {
|
|
t.Parallel()
|
|
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, hclog.NewNullLogger(), 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{
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
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},
|
|
},
|
|
)),
|
|
}
|
|
|
|
refreshWaitChs := &testRefreshWaitChs{
|
|
firstSempahore: make(chan struct{}),
|
|
secondSemaphore: make(chan struct{}),
|
|
}
|
|
wg := new(sync.WaitGroup)
|
|
wg.Add(2)
|
|
extraOpts := []Option{WithTestRefreshWaitChs(refreshWaitChs), WithIgnoreSearchStaleness(true)}
|
|
go func() {
|
|
defer wg.Done()
|
|
blockingRefreshError := rs.RefreshForSearch(ctx, at.Id, Targets, append(opts, extraOpts...)...)
|
|
assert.NoError(t, blockingRefreshError)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
// Sleep here to ensure ordering of the calls since both goroutines
|
|
// are spawned at the same time
|
|
<-refreshWaitChs.firstSempahore
|
|
nonblockingRefreshError := rs.RefreshForSearch(ctx, at.Id, Targets, append(opts, extraOpts...)...)
|
|
close(refreshWaitChs.secondSemaphore)
|
|
assert.ErrorIs(t, nonblockingRefreshError, ErrRefreshInProgress)
|
|
}()
|
|
wg.Wait()
|
|
|
|
// Unlike in the TestRefreshForSearch test, since we did a force
|
|
// refresh we do expect to see values
|
|
cachedTargets, err := r.ListTargets(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retTargets[:3], cachedTargets.Targets)
|
|
})
|
|
|
|
t.Run("sessions refreshed for searching", func(t *testing.T) {
|
|
t.Parallel()
|
|
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, hclog.NewNullLogger(), 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{
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
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},
|
|
},
|
|
)),
|
|
}
|
|
|
|
refreshWaitChs := &testRefreshWaitChs{
|
|
firstSempahore: make(chan struct{}),
|
|
secondSemaphore: make(chan struct{}),
|
|
}
|
|
wg := new(sync.WaitGroup)
|
|
wg.Add(2)
|
|
extraOpts := []Option{WithTestRefreshWaitChs(refreshWaitChs), WithIgnoreSearchStaleness(true)}
|
|
go func() {
|
|
defer wg.Done()
|
|
blockingRefreshError := rs.RefreshForSearch(ctx, at.Id, Sessions, append(opts, extraOpts...)...)
|
|
assert.NoError(t, blockingRefreshError)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
// Sleep here to ensure ordering of the calls since both goroutines
|
|
// are spawned at the same time
|
|
<-refreshWaitChs.firstSempahore
|
|
nonblockingRefreshError := rs.RefreshForSearch(ctx, at.Id, Sessions, append(opts, extraOpts...)...)
|
|
close(refreshWaitChs.secondSemaphore)
|
|
assert.ErrorIs(t, nonblockingRefreshError, ErrRefreshInProgress)
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
// Unlike in the TestRefreshForSearch test, since we are did a force
|
|
// refresh we do expect to see values
|
|
cachedSessions, err := r.ListSessions(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retSess[:3], cachedSessions.Sessions)
|
|
})
|
|
|
|
t.Run("aliases refreshed for searching", func(t *testing.T) {
|
|
t.Parallel()
|
|
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, hclog.Default(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
retAl := []*aliases.Alias{
|
|
alias("1"),
|
|
alias("2"),
|
|
alias("3"),
|
|
alias("4"),
|
|
}
|
|
opts := []Option{
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t,
|
|
[][]*aliases.Alias{
|
|
retAl[:3],
|
|
retAl[3:],
|
|
},
|
|
[][]string{
|
|
nil,
|
|
{retAl[0].Id, retAl[1].Id},
|
|
},
|
|
)),
|
|
}
|
|
|
|
refreshWaitChs := &testRefreshWaitChs{
|
|
firstSempahore: make(chan struct{}),
|
|
secondSemaphore: make(chan struct{}),
|
|
}
|
|
wg := new(sync.WaitGroup)
|
|
wg.Add(2)
|
|
extraOpts := []Option{WithTestRefreshWaitChs(refreshWaitChs), WithIgnoreSearchStaleness(true)}
|
|
go func() {
|
|
defer wg.Done()
|
|
blockingRefreshError := rs.RefreshForSearch(ctx, at.Id, ResolvableAliases, append(opts, extraOpts...)...)
|
|
assert.NoError(t, blockingRefreshError)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
// Sleep here to ensure ordering of the calls since both goroutines
|
|
// are spawned at the same time
|
|
<-refreshWaitChs.firstSempahore
|
|
nonblockingRefreshError := rs.RefreshForSearch(ctx, at.Id, ResolvableAliases, append(opts, extraOpts...)...)
|
|
close(refreshWaitChs.secondSemaphore)
|
|
assert.ErrorIs(t, nonblockingRefreshError, ErrRefreshInProgress)
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
// Unlike in the TestRefreshForSearch test, since we are did a force
|
|
// refresh we do expect to see values
|
|
cachedAliases, err := r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, retAl[:3], cachedAliases.ResolvableAliases)
|
|
})
|
|
}
|
|
|
|
func TestRefresh(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("set 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))
|
|
require.NoError(t, err)
|
|
rs, err := NewRefreshService(ctx, r, hclog.NewNullLogger(), 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"),
|
|
}
|
|
opts := []Option{
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
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.Refresh(ctx, opts...))
|
|
|
|
cachedTargets, err := r.ListTargets(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retTargets[:3], cachedTargets.Targets)
|
|
|
|
// Second call removes the first 2 resources from the cache and adds the last
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedTargets, err = r.ListTargets(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retTargets[2:], cachedTargets.Targets)
|
|
})
|
|
|
|
t.Run("set 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))
|
|
require.NoError(t, err)
|
|
rs, err := NewRefreshService(ctx, r, hclog.NewNullLogger(), 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{
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
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},
|
|
},
|
|
)),
|
|
}
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedSessions, err := r.ListSessions(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retSess[:3], cachedSessions.Sessions)
|
|
|
|
// Second call removes the first 2 resources from the cache and adds the last
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedSessions, err = r.ListSessions(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retSess[2:], cachedSessions.Sessions)
|
|
})
|
|
|
|
t.Run("set aliases", 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, hclog.Default(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
retAls := []*aliases.Alias{
|
|
alias("1"),
|
|
alias("2"),
|
|
alias("3"),
|
|
alias("4"),
|
|
}
|
|
opts := []Option{
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t,
|
|
[][]*aliases.Alias{
|
|
retAls[:3],
|
|
retAls[3:],
|
|
},
|
|
[][]string{
|
|
nil,
|
|
{retAls[0].Id, retAls[1].Id},
|
|
},
|
|
)),
|
|
}
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedAliases, err := r.ListResolvableAliases(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retAls[:3], cachedAliases.ResolvableAliases)
|
|
|
|
// Second call removes the first 2 resources from the cache and adds the last
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedAliases, err = r.ListResolvableAliases(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retAls[2:], cachedAliases.ResolvableAliases)
|
|
})
|
|
|
|
t.Run("error propagates up", 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, hclog.NewNullLogger(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
innerErr := errors.New("test error")
|
|
err = rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
|
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
|
|
}))
|
|
assert.ErrorContains(t, err, innerErr.Error())
|
|
err = rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
|
|
WithSessionRetrievalFunc(func(ctx context.Context, addr, token string, refreshTok RefreshTokenValue) ([]*sessions.Session, []string, RefreshTokenValue, error) {
|
|
require.Equal(t, boundaryAddr, addr)
|
|
require.Equal(t, at.Token, token)
|
|
return nil, nil, "", innerErr
|
|
}))
|
|
assert.ErrorContains(t, err, innerErr.Error())
|
|
})
|
|
|
|
t.Run("tokens that are no longer in the ring is deleted", 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, hclog.NewNullLogger(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
// 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
|
|
// cleaned up.
|
|
delete(atMap, ringToken{"k", "t"})
|
|
|
|
ps, err := r.listTokens(ctx, u)
|
|
require.NoError(t, err)
|
|
assert.Len(t, ps, 1)
|
|
|
|
us, err := r.listUsers(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, us, 1)
|
|
|
|
require.NoError(t, rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil)),
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil))))
|
|
|
|
ps, err = r.listTokens(ctx, u)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, ps)
|
|
|
|
// And since the last token was deleted, the user also was deleted
|
|
us, err = r.listUsers(ctx)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, us)
|
|
})
|
|
}
|
|
|
|
func TestRecheckCachingSupport(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", 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, hclog.NewNullLogger(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
// 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.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t))))
|
|
|
|
got, err := r.ListTargets(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
|
|
err = rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
|
|
assert.ErrorIs(t, err, ErrRefreshNotSupported)
|
|
|
|
got, err = r.ListTargets(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
|
|
// now a full fetch will work since the user has resources and no refresh token
|
|
assert.NoError(t, rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](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))
|
|
require.NoError(t, err)
|
|
rs, err := NewRefreshService(ctx, r, hclog.NewNullLogger(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
assert.NoError(t, rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
|
|
|
|
got, err := r.ListSessions(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
|
|
err = rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
|
|
assert.ErrorIs(t, err, ErrRefreshNotSupported)
|
|
|
|
got, err = r.ListSessions(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
|
|
assert.NoError(t, rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
|
|
got, err = r.ListSessions(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
})
|
|
|
|
t.Run("aliases", 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, hclog.Default(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
assert.NoError(t, rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
|
|
|
|
got, err := r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
|
|
err = rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
|
|
assert.ErrorIs(t, err, ErrRefreshNotSupported)
|
|
|
|
got, err = r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
|
|
assert.NoError(t, rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
|
|
got, err = r.ListResolvableAliases(ctx, at.Id)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got.Targets)
|
|
assert.Empty(t, got.ResolvableAliases)
|
|
assert.Empty(t, got.Sessions)
|
|
assert.False(t, got.Incomplete)
|
|
})
|
|
|
|
t.Run("error propagates up", 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, hclog.NewNullLogger(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
err = rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
|
|
assert.ErrorIs(t, err, ErrRefreshNotSupported)
|
|
|
|
innerErr := errors.New("test error")
|
|
err = rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
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
|
|
}))
|
|
assert.ErrorContains(t, err, innerErr.Error())
|
|
|
|
err = rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
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
|
|
}))
|
|
assert.ErrorContains(t, err, innerErr.Error())
|
|
})
|
|
|
|
t.Run("tokens that are no longer in the ring is deleted", 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, hclog.NewNullLogger(), 0, 0)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
err = rs.Refresh(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
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
|
|
// cleaned up.
|
|
delete(atMap, ringToken{"k", "t"})
|
|
|
|
ps, err := r.listTokens(ctx, u)
|
|
require.NoError(t, err)
|
|
assert.Len(t, ps, 1)
|
|
|
|
us, err := r.listUsers(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, us, 1)
|
|
|
|
err = rs.RecheckCachingSupport(ctx,
|
|
WithAliasRetrievalFunc(testNoRefreshRetrievalFuncForId[*aliases.Alias](t)),
|
|
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
|
|
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
|
|
assert.NoError(t, err)
|
|
|
|
ps, err = r.listTokens(ctx, u)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, ps)
|
|
|
|
// And since the last token was deleted, the user also was deleted
|
|
us, err = r.listUsers(ctx)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, us)
|
|
})
|
|
}
|
|
|
|
func target(suffix string) *targets.Target {
|
|
return &targets.Target{
|
|
Id: fmt.Sprintf("target_%s", suffix),
|
|
Name: fmt.Sprintf("name_%s", suffix),
|
|
Description: fmt.Sprintf("description_%s", suffix),
|
|
Address: fmt.Sprintf("address_%s", suffix),
|
|
ScopeId: fmt.Sprintf("p_%s", suffix),
|
|
Type: "tcp",
|
|
SessionMaxSeconds: 1234,
|
|
}
|
|
}
|
|
|
|
func session(suffix string) *sessions.Session {
|
|
return &sessions.Session{
|
|
Id: fmt.Sprintf("session_%s", suffix),
|
|
Type: "tcp",
|
|
}
|
|
}
|
|
|
|
func alias(suffix string) *aliases.Alias {
|
|
return &aliases.Alias{
|
|
Id: fmt.Sprintf("alt_%s", suffix),
|
|
Type: "target",
|
|
Value: fmt.Sprintf("value%s", suffix),
|
|
}
|
|
}
|