Add aliases to the client cache daemon and make them searchable (#4418)

* Add aliases to the client cache daemon and make them searchable
This commit is contained in:
Todd committed 2024-02-29 15:52:27 -08:00
1 parent 2e55ae72ba
commit a2ea1f8f90
16 files changed
+1359 -18

No files matched your search

+72
View File
@@ -13,6 +13,7 @@ import (
"time"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
daemoncmd "github.com/hashicorp/boundary/internal/clientcache/cmd/daemon"
@@ -29,6 +30,7 @@ var (
_ cli.CommandAutocomplete = (*SearchCommand)(nil)
supportedResourceTypes = []string{
"aliases",
"targets",
"sessions",
}
@@ -163,6 +165,8 @@ func (c *SearchCommand) Run(args []string) int {
}
default:
switch {
case len(result.Aliases) > 0:
c.UI.Output(printAliasListTable(result.Aliases))
case len(result.Targets) > 0:
c.UI.Output(printTargetListTable(result.Targets))
case len(result.Sessions) > 0:
@@ -241,6 +245,74 @@ func search(ctx context.Context, daemonPath string, fb filterBy, opt ...client.O
return resp, res, nil, nil
}
func printAliasListTable(items []*aliases.Alias) string {
if len(items) == 0 {
return "No aliases found"
}
var output []string
output = []string{
"",
"Alias information:",
}
for i, item := range items {
if i > 0 {
output = append(output, "")
}
if item.Id != "" {
output = append(output,
fmt.Sprintf(" ID: %s", item.Id),
)
} else {
output = append(output,
fmt.Sprintf(" ID: %s", "(not available)"),
)
}
if item.ScopeId != "" {
output = append(output,
fmt.Sprintf(" Scope ID: %s", item.ScopeId),
)
}
if item.Version > 0 {
output = append(output,
fmt.Sprintf(" Version: %d", item.Version),
)
}
if item.Type != "" {
output = append(output,
fmt.Sprintf(" Type: %s", item.Type),
)
}
if item.Name != "" {
output = append(output,
fmt.Sprintf(" Name: %s", item.Name),
)
}
if item.Description != "" {
output = append(output,
fmt.Sprintf(" Description: %s", item.Description),
)
}
if item.DestinationId != "" {
output = append(output,
fmt.Sprintf(" DestinationId: %s", item.DestinationId),
)
}
if item.Value != "" {
output = append(output,
fmt.Sprintf(" Value: %s", item.Value),
)
}
if len(item.AuthorizedActions) > 0 {
output = append(output,
" Authorized Actions:",
base.WrapSlice(6, item.AuthorizedActions),
)
}
}
return base.WrapForHelpText(output)
}
func printTargetListTable(items []*targets.Target) string {
if len(items) == 0 {
return "No targets found"
@@ -11,6 +11,7 @@ import (
"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"
@@ -166,7 +167,10 @@ func TestSearch(t *testing.T) {
assert.Contains(t, apiErr.Message, "doesn't support search")
})
srv.AddResources(t, at, []*targets.Target{
srv.AddResources(t, at, []*aliases.Alias{
{Id: "alt_1234567890", Value: "value1", DestinationId: "ttcp_1234567890"},
{Id: "alt_0987654321", Name: "value2", DestinationId: "ttcp_0987654321"},
}, []*targets.Target{
{Id: "ttcp_1234567890", Name: "name1", Description: "description1"},
{Id: "ttcp_0987654321", Name: "name2", Description: "description2"},
}, []*sessions.Session{
+9
View File
@@ -12,6 +12,7 @@ type options struct {
withDbType dbw.DbType
withAuthTokenId string
withUserId string
withAliasRetrievalFunc AliasRetrievalFunc
withTargetRetrievalFunc TargetRetrievalFunc
withSessionRetrievalFunc SessionRetrievalFunc
withIgnoreSearchStaleness bool
@@ -61,6 +62,14 @@ func withUserId(id string) Option {
}
}
// WithAliasRetrievalFunc provides an option for specifying an aliasRetrievalFunc
func WithAliasRetrievalFunc(fn AliasRetrievalFunc) Option {
return func(o *options) error {
o.withAliasRetrievalFunc = fn
return nil
}
}
// WithTargetRetrievalFunc provides an option for specifying a targetRetrievalFunc
func WithTargetRetrievalFunc(fn TargetRetrievalFunc) Option {
return func(o *options) error {
+14
View File
@@ -7,6 +7,7 @@ import (
"context"
"testing"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/go-dbw"
@@ -72,6 +73,19 @@ func Test_GetOpts(t *testing.T) {
testOpts := getDefaultOptions()
assert.Equal(t, opts, testOpts)
})
t.Run("WithAliasRetrievalFunc", func(t *testing.T) {
var f AliasRetrievalFunc = func(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) ([]*aliases.Alias, []string, RefreshTokenValue, error) {
return nil, nil, "", nil
}
opts, err := getOpts(WithAliasRetrievalFunc(f))
require.NoError(t, err)
assert.NotNil(t, opts.withAliasRetrievalFunc)
opts.withAliasRetrievalFunc = nil
testOpts := getDefaultOptions()
assert.Equal(t, opts, testOpts)
})
t.Run("withIgnoreSearchStaleness", func(t *testing.T) {
opts, err := getOpts(WithIgnoreSearchStaleness(true))
require.NoError(t, err)
+20
View File
@@ -197,6 +197,16 @@ func (r *RefreshService) RefreshForSearch(ctx context.Context, authTokenid strin
}
switch resourceType {
case Aliases:
rtv, err := r.repo.lookupRefreshToken(ctx, u, aliasResourceType)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if opts.withIgnoreSearchStaleness || rtv != nil && time.Since(rtv.UpdateTime) > r.maxSearchStaleness {
if err := r.repo.refreshAliases(ctx, u, tokens, opt...); err != nil {
return errors.Wrap(ctx, err, op)
}
}
case Targets:
rtv, err := r.repo.lookupRefreshToken(ctx, u, targetResourceType)
if err != nil {
@@ -260,6 +270,9 @@ func (r *RefreshService) Refresh(ctx context.Context, opt ...Option) error {
continue
}
if err := r.repo.refreshAliases(ctx, u, tokens, opt...); err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for user id %s", u.Id))))
}
if err := r.repo.refreshTargets(ctx, u, tokens, opt...); err != nil {
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for user id %s", u.Id))))
}
@@ -327,6 +340,13 @@ func (r *RefreshService) RecheckCachingSupport(ctx context.Context, opt ...Optio
}
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for user id %s", u.Id))))
}
if err := r.repo.checkCachingAliases(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))))
}
}
return retErr
+214
View File
@@ -13,6 +13,7 @@ import (
"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"
@@ -367,6 +368,7 @@ func TestRefreshForSearch(t *testing.T) {
target("4"),
}
opts := []Option{
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*aliases.Alias](t, nil, nil)),
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t,
[][]*targets.Target{
@@ -418,6 +420,7 @@ func TestRefreshForSearch(t *testing.T) {
target("4"),
}
opts := []Option{
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*aliases.Alias](t, nil, nil)),
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t,
[][]*targets.Target{
@@ -472,6 +475,7 @@ func TestRefreshForSearch(t *testing.T) {
// Get the first set of resources, but no refresh tokens
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*aliases.Alias](t, nil, nil)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.ErrorContains(t, err, ErrRefreshNotSupported.Error())
@@ -484,11 +488,13 @@ func TestRefreshForSearch(t *testing.T) {
// wont be refreshed any more, and we wont see the error when refreshing
// any more.
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.Nil(t, err)
err = rs.RecheckCachingSupport(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.Nil(t, err)
@@ -500,6 +506,7 @@ func TestRefreshForSearch(t *testing.T) {
// Now simulate the controller updating to support refresh tokens and
// the resources starting to be cached.
err = rs.RecheckCachingSupport(ctx,
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*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)
@@ -525,6 +532,7 @@ func TestRefreshForSearch(t *testing.T) {
session("4"),
}
opts := []Option{
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*aliases.Alias](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t,
[][]*sessions.Session{
@@ -573,6 +581,7 @@ func TestRefreshForSearch(t *testing.T) {
session("4"),
}
opts := []Option{
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*aliases.Alias](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t,
[][]*sessions.Session{
@@ -609,6 +618,109 @@ func TestRefreshForSearch(t *testing.T) {
assert.NoError(t, err)
assert.ElementsMatch(t, retSess[2:], cachedSessions)
})
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(testStaticResourceRetrievalFunc[*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, Aliases, opts...))
cachedAliases, err := r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, cachedAliases)
assert.NoError(t, rs.Refresh(ctx, opts...))
cachedAliases, err = r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retAl[:3], cachedAliases)
// Second call removes the first 2 resources from the cache and adds the last
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Aliases, opts...))
cachedAliases, err = r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retAl[2:], cachedAliases)
})
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(testStaticResourceRetrievalFunc[*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, Aliases, opts...))
cachedAliases, err := r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, cachedAliases)
assert.NoError(t, rs.Refresh(ctx, opts...))
cachedAliases, err = r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retAls[:3], cachedAliases)
// Refresh for search doesn't refresh anything because it isn't stale
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Aliases, opts...))
cachedAliases, err = r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retAls[:3], cachedAliases)
// Now force the refresh and see things get updated
assert.NoError(t, rs.RefreshForSearch(ctx, at.Id, Aliases, append(opts, WithIgnoreSearchStaleness(true))...))
cachedAliases, err = r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retAls[2:], cachedAliases)
})
}
func TestRefresh(t *testing.T) {
@@ -644,6 +756,7 @@ func TestRefresh(t *testing.T) {
target("4"),
}
opts := []Option{
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*aliases.Alias](t, nil, nil)),
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t,
[][]*targets.Target{
@@ -685,6 +798,7 @@ func TestRefresh(t *testing.T) {
session("4"),
}
opts := []Option{
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*aliases.Alias](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil)),
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t,
[][]*sessions.Session{
@@ -709,6 +823,47 @@ func TestRefresh(t *testing.T) {
assert.ElementsMatch(t, retSess[2:], cachedSessions)
})
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(testStaticResourceRetrievalFunc[*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.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retAls[:3], cachedAliases)
// Second call removes the first 2 resources from the cache and adds the last
assert.NoError(t, rs.Refresh(ctx, opts...))
cachedAliases, err = r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.ElementsMatch(t, retAls[2:], cachedAliases)
})
t.Run("error propagates up", func(t *testing.T) {
s, err := db.Open(ctx)
require.NoError(t, err)
@@ -720,6 +875,7 @@ func TestRefresh(t *testing.T) {
innerErr := errors.New("test error")
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*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)
@@ -728,6 +884,7 @@ func TestRefresh(t *testing.T) {
}))
assert.ErrorContains(t, err, innerErr.Error())
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*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)
@@ -760,6 +917,7 @@ func TestRefresh(t *testing.T) {
assert.Len(t, us, 1)
require.NoError(t, rs.Refresh(ctx,
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc[*aliases.Alias](t, nil, nil)),
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil)),
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t, nil, nil))))
@@ -803,6 +961,7 @@ func TestRecheckCachingSupport(t *testing.T) {
// 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(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t))))
@@ -811,6 +970,7 @@ func TestRecheckCachingSupport(t *testing.T) {
assert.Empty(t, got)
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.ErrorIs(t, err, ErrRefreshNotSupported)
@@ -821,6 +981,7 @@ func TestRecheckCachingSupport(t *testing.T) {
// now a full fetch will work since the user has resources and no refresh token
assert.NoError(t, rs.RecheckCachingSupport(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t))))
})
@@ -835,6 +996,7 @@ func TestRecheckCachingSupport(t *testing.T) {
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
assert.NoError(t, rs.RecheckCachingSupport(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
@@ -843,6 +1005,7 @@ func TestRecheckCachingSupport(t *testing.T) {
assert.Empty(t, got)
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
assert.ErrorIs(t, err, ErrRefreshNotSupported)
@@ -852,6 +1015,7 @@ func TestRecheckCachingSupport(t *testing.T) {
assert.Empty(t, got)
assert.NoError(t, rs.RecheckCachingSupport(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
got, err = r.ListSessions(ctx, at.Id)
@@ -859,6 +1023,43 @@ func TestRecheckCachingSupport(t *testing.T) {
assert.Empty(t, got)
})
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(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
got, err := r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, got)
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
assert.ErrorIs(t, err, ErrRefreshNotSupported)
got, err = r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, got)
assert.NoError(t, rs.RecheckCachingSupport(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t))))
got, err = r.ListAliases(ctx, at.Id)
assert.NoError(t, err)
assert.Empty(t, got)
})
t.Run("error propagates up", func(t *testing.T) {
s, err := db.Open(ctx)
require.NoError(t, err)
@@ -869,12 +1070,14 @@ func TestRecheckCachingSupport(t *testing.T) {
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*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(testNoRefreshRetrievalFunc[*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)
@@ -884,6 +1087,7 @@ func TestRecheckCachingSupport(t *testing.T) {
assert.ErrorContains(t, err, innerErr.Error())
err = rs.RecheckCachingSupport(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*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)
@@ -903,6 +1107,7 @@ func TestRecheckCachingSupport(t *testing.T) {
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
err = rs.Refresh(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
assert.ErrorIs(t, err, ErrRefreshNotSupported)
@@ -921,6 +1126,7 @@ func TestRecheckCachingSupport(t *testing.T) {
assert.Len(t, us, 1)
err = rs.RecheckCachingSupport(ctx,
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)),
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)),
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
assert.NoError(t, err)
@@ -954,3 +1160,11 @@ func session(suffix string) *sessions.Session {
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),
}
}
@@ -0,0 +1,373 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cache
import (
"context"
"database/sql"
"encoding/json"
stderrors "errors"
"fmt"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/event"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/mql"
)
// AliasRetrievalFunc is a function that retrieves aliases
// from the provided boundary addr using the provided token.
type AliasRetrievalFunc func(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) (ret []*aliases.Alias, removedIds []string, refreshToken RefreshTokenValue, err error)
func defaultAliasFunc(ctx context.Context, addr, authTok string, refreshTok RefreshTokenValue) ([]*aliases.Alias, []string, RefreshTokenValue, error) {
const op = "cache.defaultAliasFunc"
client, err := api.NewClient(&api.Config{
Addr: addr,
Token: authTok,
})
if err != nil {
return nil, nil, "", errors.Wrap(ctx, err, op)
}
aClient := aliases.NewClient(client)
l, err := aClient.List(ctx, "global", aliases.WithRecursive(true), aliases.WithListToken(string(refreshTok)))
if err != nil {
if api.ErrInvalidListToken.Is(err) {
return nil, nil, "", err
}
return nil, nil, "", errors.Wrap(ctx, err, op)
}
if l.ResponseType == "" {
return nil, nil, "", ErrRefreshNotSupported
}
return l.Items, l.RemovedIds, RefreshTokenValue(l.ListToken), nil
}
// refreshAliases attempts to refresh the aliases for the provided user
// using the provided tokens. If available, it uses the refresh tokens in
// storage to retrieve and apply only the delta.
func (r *Repository) refreshAliases(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
const op = "cache.(Repository).refreshAliases"
switch {
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 missing")
case u.Address == "":
return errors.New(ctx, errors.InvalidParameter, op, "user boundary address is missing")
}
const resourceType = aliasResourceType
opts, err := getOpts(opt...)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if opts.withAliasRetrievalFunc == nil {
opts.withAliasRetrievalFunc = defaultAliasFunc
}
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 aliases
var gotResponse bool
var resp []*aliases.Alias
var newRefreshToken RefreshTokenValue
var unsupportedCacheRequest bool
var removedIds []string
var retErr error
for at, t := range tokens {
resp, removedIds, newRefreshToken, err = opts.withAliasRetrievalFunc(ctx, u.Address, t, oldRefreshTokenVal)
if api.ErrInvalidListToken.Is(err) {
event.WriteSysEvent(ctx, op, "old list token is no longer valid, starting new initial fetch", "user_id", u.Id)
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, newRefreshToken, err = opts.withAliasRetrievalFunc(ctx, u.Address, t, "")
}
if err != nil {
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
}
if retErr != nil {
if saveErr := r.saveError(r.serverCtx, u, resourceType, retErr); saveErr != nil {
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
}
}
if !gotResponse {
return retErr
}
var numDeleted int
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error {
var err error
switch {
case oldRefreshToken == nil:
if numDeleted, err = w.Exec(ctx, "delete from alias where fk_user_id = @fk_user_id",
[]any{sql.Named("fk_user_id", u.Id)}); err != nil {
return err
}
case len(removedIds) > 0:
if numDeleted, err = w.Exec(ctx, "delete from alias where id in @ids",
[]any{sql.Named("ids", removedIds)}); err != nil {
return err
}
}
switch {
case unsupportedCacheRequest:
if err := upsertRefreshToken(ctx, w, u, resourceType, sentinelNoRefreshToken); err != nil {
return err
}
case newRefreshToken != "":
if err := upsertAliases(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
}
event.WriteSysEvent(ctx, op, "aliases updated", "deleted", numDeleted, "upserted", len(resp), "user_id", u.Id)
return nil
}
// checkCachingAliases fetches all aliases 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) checkCachingAliases(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
const op = "cache.(Repository).checkAliasesForSearchability"
switch {
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 missing")
case u.Address == "":
return errors.New(ctx, errors.InvalidParameter, op, "user boundary address is missing")
}
const resourceType = aliasResourceType
opts, err := getOpts(opt...)
if err != nil {
return errors.Wrap(ctx, err, op)
}
if opts.withAliasRetrievalFunc == nil {
opts.withAliasRetrievalFunc = defaultAliasFunc
}
// Find and use a token for retrieving aliases
var gotResponse bool
var resp []*aliases.Alias
var newRefreshToken RefreshTokenValue
var unsupportedCacheRequest bool
var retErr error
for at, t := range tokens {
resp, _, newRefreshToken, err = opts.withAliasRetrievalFunc(ctx, u.Address, t, "")
if err != nil {
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
}
if retErr != nil {
if saveErr := r.saveError(r.serverCtx, u, resourceType, retErr); saveErr != nil {
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
}
}
if !gotResponse {
return retErr
}
var numDeleted int
_, 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 != "":
var err error
if numDeleted, err = w.Exec(ctx, "delete from alias where fk_user_id = @fk_user_id",
[]any{sql.Named("fk_user_id", u.Id)}); err != nil {
return err
}
if err := upsertAliases(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
}
event.WriteSysEvent(ctx, op, "aliases updated", "deleted", numDeleted, "upserted", len(resp), "user_id", u.Id)
return nil
}
// upsertAliases upserts the provided aliases to be stored for the provided user.
func upsertAliases(ctx context.Context, w db.Writer, u *user, in []*aliases.Alias) error {
const op = "cache.upsertAliases"
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")
}
for _, s := range in {
item, err := json.Marshal(s)
if err != nil {
return errors.Wrap(ctx, err, op)
}
newAlias := &Alias{
FkUserId: u.Id,
Id: s.Id,
Type: s.Type,
ScopeId: s.ScopeId,
DestinationId: s.DestinationId,
Value: s.Value,
Item: string(item),
}
onConflict := db.OnConflict{
Target: db.Columns{"fk_user_id", "id"},
Action: db.SetColumns([]string{"type", "scope_id", "destination_id", "value", "item"}),
}
if err := w.Create(ctx, newAlias, db.WithOnConflict(&onConflict)); err != nil {
return errors.Wrap(ctx, err, op)
}
}
return nil
}
func (r *Repository) ListAliases(ctx context.Context, authTokenId string) ([]*aliases.Alias, error) {
const op = "cache.(Repository).ListAliases"
switch {
case authTokenId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is missing")
}
ret, err := r.searchAliases(ctx, "true", nil, withAuthTokenId(authTokenId))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return ret, nil
}
func (r *Repository) QueryAliases(ctx context.Context, authTokenId, query string) ([]*aliases.Alias, error) {
const op = "cache.(Repository).QueryAliases"
switch {
case authTokenId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is missing")
case query == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "query is missing")
}
w, err := mql.Parse(query, Alias{}, mql.WithIgnoredFields("FkUserId", "Item"))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
}
ret, err := r.searchAliases(ctx, w.Condition, w.Args, withAuthTokenId(authTokenId))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return ret, nil
}
func (r *Repository) searchAliases(ctx context.Context, condition string, searchArgs []any, opt ...Option) ([]*aliases.Alias, error) {
const op = "cache.(Repository).searchAliases"
switch {
case condition == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "condition is missing")
}
opts, err := getOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
switch {
case opts.withAuthTokenId != "" && opts.withUserId != "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "both user id and auth token id were provided")
case opts.withAuthTokenId == "" && opts.withUserId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "neither user id nor auth token id were provided")
case opts.withAuthTokenId != "":
condition = fmt.Sprintf("%s and fk_user_id in (select user_id from auth_token where id = ?)", condition)
searchArgs = append(searchArgs, opts.withAuthTokenId)
case opts.withUserId != "":
condition = fmt.Sprintf("%s and fk_user_id = ?", condition)
searchArgs = append(searchArgs, opts.withUserId)
}
var cachedAliases []*Alias
if err := r.rw.SearchWhere(ctx, &cachedAliases, condition, searchArgs, db.WithLimit(-1)); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
retAliases := make([]*aliases.Alias, 0, len(cachedAliases))
for _, cachedSess := range cachedAliases {
var sess aliases.Alias
if err := json.Unmarshal([]byte(cachedSess.Item), &sess); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
retAliases = append(retAliases, &sess)
}
return retAliases, nil
}
type Alias struct {
FkUserId string `gorm:"primaryKey"`
Id string `gorm:"primaryKey"`
Type string `gorm:"default:null"`
ScopeId string `gorm:"default:null"`
DestinationId string `gorm:"default:null"`
Value string `gorm:"default:null"`
Item string `gorm:"default:null"`
}
func (*Alias) TableName() string {
return "alias"
}
@@ -0,0 +1,480 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cache
import (
"context"
"encoding/json"
"sync"
"testing"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/globals"
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
"github.com/hashicorp/boundary/internal/daemon/controller"
"github.com/hashicorp/boundary/internal/db"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets/tcp"
)
func TestRepository_refreshAliases(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "keyring",
TokenName: "token",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{kt.KeyringType, kt.TokenName}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
als := []*aliases.Alias{
{
Id: "alt_1",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "example",
Type: "target",
},
{
Id: "alt_2",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "example2",
Type: "target",
},
{
Id: "alt_3",
ScopeId: "global",
DestinationId: "ttcp_123",
Value: "example3",
Type: "target",
},
}
var want []*Alias
for _, al := range als {
si, err := json.Marshal(al)
require.NoError(t, err)
want = append(want, &Alias{
FkUserId: u.Id,
Id: al.Id,
Type: al.Type,
ScopeId: al.ScopeId,
DestinationId: al.DestinationId,
Value: al.Value,
Item: string(si),
})
}
cases := []struct {
name string
u *user
al []*aliases.Alias
want []*Alias
errorContains string
}{
{
name: "Success",
u: &user{
Address: addr,
Id: at.UserId,
},
al: als,
want: want,
},
{
name: "repeated alias with different values",
u: &user{
Address: addr,
Id: at.UserId,
},
al: append(als, &aliases.Alias{
Id: als[0].Id,
Value: "different.value",
}),
want: append(want[1:], &Alias{
FkUserId: want[0].FkUserId,
Id: want[0].Id,
Value: "different.value",
Item: `{"id":"alt_1","created_time":"0001-01-01T00:00:00Z","updated_time":"0001-01-01T00:00:00Z","value":"different.value"}`,
}),
},
{
name: "nil user",
u: nil,
al: als,
errorContains: "user is nil",
},
{
name: "missing user Id",
u: &user{
Address: addr,
},
al: als,
errorContains: "user id is missing",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := r.refreshAliases(ctx, tc.u, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*aliases.Alias{tc.al}, [][]string{nil})))
if tc.errorContains == "" {
assert.NoError(t, err)
rw := db.New(s)
var got []*Alias
require.NoError(t, rw.SearchWhere(ctx, &got, "true", nil))
assert.ElementsMatch(t, got, tc.want)
t.Cleanup(func() {
refTok := &refreshToken{
UserId: tc.u.Id,
ResourceType: aliasResourceType,
}
_, err := r.rw.Delete(ctx, refTok)
require.NoError(t, err)
})
} else {
assert.ErrorContains(t, err, tc.errorContains)
}
})
}
}
func TestRepository_RefreshAliases_withRefreshTokens(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u := user{
Id: "u1",
Address: addr,
}
at := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u.Id,
}
kt := KeyringToken{
KeyringType: "keyring",
TokenName: "token",
AuthTokenId: at.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{kt.KeyringType, kt.TokenName}: at,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt))
ss := [][]*aliases.Alias{
{
{
Id: "alt_1",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "u_123",
Type: "tcp",
},
{
Id: "alt_2",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "u_123",
Type: "tcp",
},
},
{
{
Id: "alt_3",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "u_123",
Type: "tcp",
},
},
}
err = r.refreshAliases(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc(t, ss, [][]string{nil, nil})))
assert.NoError(t, err)
got, err := r.ListAliases(ctx, at.Id)
require.NoError(t, err)
assert.Len(t, got, 2)
// Refreshing again uses the refresh token and get additional aliases, appending
// them to the response
err = r.refreshAliases(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc(t, ss, [][]string{nil, nil})))
assert.NoError(t, err)
got, err = r.ListAliases(ctx, at.Id)
require.NoError(t, err)
assert.Len(t, got, 3)
// Refreshing again wont return any more resources, but also none should be
// removed
require.NoError(t, r.refreshAliases(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc(t, ss, [][]string{nil, nil}))))
assert.NoError(t, err)
got, err = r.ListAliases(ctx, at.Id)
require.NoError(t, err)
assert.Len(t, got, 3)
// Refresh again with the refresh token being reported as invalid.
require.NoError(t, r.refreshAliases(ctx, &u, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testErroringForRefreshTokenRetrievalFunc(t, ss[0]))))
assert.NoError(t, err)
got, err = r.ListAliases(ctx, at.Id)
require.NoError(t, err)
assert.Len(t, got, 2)
}
func TestRepository_ListAliases(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u1 := &user{
Id: "u1",
Address: addr,
}
at1 := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u1.Id,
}
kt1 := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at1.Id,
}
u2 := &user{
Id: "u2",
Address: addr,
}
at2 := &authtokens.AuthToken{
Id: "at_2",
Token: "at_2_token",
UserId: u2.Id,
}
kt2 := KeyringToken{
KeyringType: "k2",
TokenName: "t2",
AuthTokenId: at2.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at1,
{"k2", "t2"}: at2,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
require.NoError(t, r.AddKeyringToken(ctx, addr, kt2))
t.Run("auth token id is missing", func(t *testing.T) {
l, err := r.ListAliases(ctx, "")
assert.Nil(t, l)
assert.ErrorContains(t, err, "auth token id is missing")
})
ss := []*aliases.Alias{
{
Id: "alt_1",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "u_123",
Type: "tcp",
},
{
Id: "alt_2",
ScopeId: "o_123",
DestinationId: "ttcp_123",
Value: "u_123",
Type: "tcp",
},
{
Id: "alt_3",
ScopeId: "global",
DestinationId: "ttcp_123",
Value: "u_123",
Type: "tcp",
},
}
require.NoError(t, r.refreshAliases(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*aliases.Alias{ss}, [][]string{nil}))))
t.Run("wrong user gets no aliases", func(t *testing.T) {
l, err := r.ListAliases(ctx, kt2.AuthTokenId)
assert.NoError(t, err)
assert.Empty(t, l)
})
t.Run("correct token gets aliases", func(t *testing.T) {
l, err := r.ListAliases(ctx, kt1.AuthTokenId)
assert.NoError(t, err)
assert.Len(t, l, len(ss))
assert.ElementsMatch(t, l, ss)
})
}
func TestRepository_QueryAliases(t *testing.T) {
ctx := context.Background()
s, err := cachedb.Open(ctx)
require.NoError(t, err)
addr := "address"
u1 := &user{
Id: "u1",
Address: addr,
}
at1 := &authtokens.AuthToken{
Id: "at_1",
Token: "at_1_token",
UserId: u1.Id,
}
kt1 := KeyringToken{
KeyringType: "k1",
TokenName: "t1",
AuthTokenId: at1.Id,
}
u2 := &user{
Id: "u2",
Address: addr,
}
at2 := &authtokens.AuthToken{
Id: "at_2",
Token: "at_2_token",
UserId: u2.Id,
}
kt2 := KeyringToken{
KeyringType: "k2",
TokenName: "t2",
AuthTokenId: at2.Id,
}
atMap := map[ringToken]*authtokens.AuthToken{
{"k1", "t1"}: at1,
{"k2", "t2"}: at2,
}
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
require.NoError(t, err)
require.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
require.NoError(t, r.AddKeyringToken(ctx, addr, kt2))
query := `(value % "val1" or value % "val2") and destination_id % "ttcp_"`
errorCases := []struct {
name string
t string
query string
errContains string
}{
{
name: "auth token id is missing",
t: "",
query: query,
errContains: "auth token id is missing",
},
{
name: "query is missing",
t: "token id",
errContains: "query is missing",
},
}
for _, tc := range errorCases {
t.Run(tc.name, func(t *testing.T) {
l, err := r.QueryAliases(ctx, tc.t, tc.query)
assert.Nil(t, l)
assert.ErrorContains(t, err, tc.errContains)
})
}
ss := []*aliases.Alias{
{
Id: "alt_1",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "val1",
Type: "target",
},
{
Id: "alt_2",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "val2",
Type: "target",
},
{
Id: "alt_3",
ScopeId: "p_123",
DestinationId: "ttcp_123",
Value: "val3",
Type: "target",
},
}
require.NoError(t, r.refreshAliases(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*aliases.Alias{ss}, [][]string{nil}))))
t.Run("wrong token gets no aliases", func(t *testing.T) {
l, err := r.QueryAliases(ctx, kt2.AuthTokenId, query)
assert.NoError(t, err)
assert.Empty(t, l)
})
t.Run("correct token gets aliases", func(t *testing.T) {
l, err := r.QueryAliases(ctx, kt1.AuthTokenId, query)
assert.NoError(t, err)
assert.Len(t, l, 2)
assert.ElementsMatch(t, l, ss[0:2])
})
}
func TestDefaultAliasRetrievalFunc(t *testing.T) {
oldDur := globals.RefreshReadLookbackDuration
globals.RefreshReadLookbackDuration = 0
t.Cleanup(func() {
globals.RefreshReadLookbackDuration = oldDur
})
tc := controller.NewTestController(t, nil)
tc.Client().SetToken(tc.Token().Token)
tarClient := aliases.NewClient(tc.Client())
al1, err := tarClient.Create(tc.Context(), "target", "global", aliases.WithName("al1"), aliases.WithValue("address"))
require.NoError(t, err)
require.NotNil(t, al1)
got, removed, refTok, err := defaultAliasFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token, "")
assert.NoError(t, err)
assert.NotEmpty(t, refTok)
assert.Empty(t, removed)
assert.Len(t, got, 1)
got2, removed2, refTok2, err := defaultAliasFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token, refTok)
assert.NoError(t, err)
assert.NotEmpty(t, refTok2)
assert.Empty(t, removed2)
assert.Empty(t, got2)
}
@@ -221,11 +221,12 @@ const (
unknownResourceType resourceType = "unknown"
targetResourceType resourceType = "target"
sessionResourceType resourceType = "session"
aliasResourceType resourceType = "alias"
)
func (r resourceType) valid() bool {
switch r {
case targetResourceType, sessionResourceType:
case aliasResourceType, targetResourceType, sessionResourceType:
return true
}
return false
+13 -1
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"strings"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/errors"
@@ -19,13 +20,14 @@ type SearchableResource string
const (
Unknown SearchableResource = "unknown"
Aliases SearchableResource = "aliases"
Targets SearchableResource = "targets"
Sessions SearchableResource = "sessions"
)
func (r SearchableResource) Valid() bool {
switch r {
case Targets, Sessions:
case Aliases, Targets, Sessions:
return true
}
return false
@@ -33,6 +35,8 @@ func (r SearchableResource) Valid() bool {
func ToSearchableResource(s string) SearchableResource {
switch {
case strings.EqualFold(s, string(Aliases)):
return Aliases
case strings.EqualFold(s, string(Targets)):
return Targets
case strings.EqualFold(s, string(Sessions)):
@@ -55,6 +59,7 @@ type SearchParams struct {
// SearchResult returns the results from searching the cache.
type SearchResult struct {
Aliases []*aliases.Alias
Targets []*targets.Target
Sessions []*sessions.Session
}
@@ -75,6 +80,13 @@ func NewSearchService(ctx context.Context, repo *Repository) (*SearchService, er
return &SearchService{
repo: repo,
searchableResources: map[SearchableResource]resourceSearcher{
Aliases: &resourceSearchFns[*aliases.Alias]{
list: repo.ListAliases,
query: repo.QueryAliases,
searchResult: func(a []*aliases.Alias) *SearchResult {
return &SearchResult{Aliases: a}
},
},
Targets: &resourceSearchFns[*targets.Target]{
list: repo.ListTargets,
query: repo.QueryTargets,
+78
View File
@@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
@@ -203,6 +204,12 @@ func TestSearch(t *testing.T) {
require.NoError(t, rw.Create(ctx, u))
require.NoError(t, rw.Create(ctx, at))
aliases := []any{
&Alias{FkUserId: u.Id, Id: "alt_1", Value: "one", Type: "target", Item: `{"id": "alt_1", "value": "one", "type": "target"}`},
&Alias{FkUserId: u.Id, Id: "alt_2", Value: "two", Type: "target", Item: `{"id": "alt_2", "value": "two", "type": "target"}`},
}
require.NoError(t, rw.CreateItems(ctx, aliases))
targets := []any{
&Target{FkUserId: u.Id, Id: "t_1", Name: "one", Type: "tcp", Item: `{"id": "t_1", "name": "one", "type": "tcp"}`},
&Target{FkUserId: u.Id, Id: "t_2", Name: "two", Type: "tcp", Item: `{"id": "t_2", "name": "two", "type": "tcp"}`},
@@ -225,6 +232,77 @@ func TestSearch(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, ss)
t.Run("List aliases", func(t *testing.T) {
got, err := ss.Search(ctx, SearchParams{
Resource: "aliases",
AuthTokenId: at.Id,
})
assert.NoError(t, err)
assert.EqualValues(t, &SearchResult{Aliases: []*aliases.Alias{
{Id: "alt_1", Value: "one", Type: "target"},
{Id: "alt_2", Value: "two", Type: "target"},
}}, got)
})
t.Run("query aliases", func(t *testing.T) {
got, err := ss.Search(ctx, SearchParams{
Resource: "aliases",
AuthTokenId: at.Id,
Query: `value="one"`,
})
assert.NoError(t, err)
assert.EqualValues(t, &SearchResult{Aliases: []*aliases.Alias{
{Id: "alt_1", Value: "one", Type: "target"},
}}, got)
})
t.Run("query aliases on type", func(t *testing.T) {
got, err := ss.Search(ctx, SearchParams{
Resource: "aliases",
AuthTokenId: at.Id,
Query: `type="target"`,
})
assert.NoError(t, err)
assert.EqualValues(t, &SearchResult{Aliases: []*aliases.Alias{
{Id: "alt_1", Value: "one", Type: "target"},
{Id: "alt_2", Value: "two", Type: "target"},
}}, got)
})
t.Run("query aliases bad column", func(t *testing.T) {
got, err := ss.Search(ctx, SearchParams{
Resource: "aliases",
AuthTokenId: at.Id,
Query: `item % "one"`,
})
assert.Error(t, err)
assert.ErrorContains(t, err, `invalid column "item"`)
assert.Nil(t, got)
})
t.Run("query aliases bad column owner user id", func(t *testing.T) {
got, err := ss.Search(ctx, SearchParams{
Resource: "aliases",
AuthTokenId: at.Id,
Query: `fk_user_id % "u"`,
})
assert.Error(t, err)
assert.ErrorContains(t, err, `invalid column "fk_user_id"`)
assert.Nil(t, got)
})
t.Run("Filter aliases", func(t *testing.T) {
got, err := ss.Search(ctx, SearchParams{
Resource: "aliases",
AuthTokenId: at.Id,
Filter: `"/item/value" matches "one"`,
})
assert.NoError(t, err)
assert.EqualValues(t, &SearchResult{Aliases: []*aliases.Alias{
{Id: "alt_1", Value: "one", Type: "target"},
}}, got)
})
t.Run("List targets", func(t *testing.T) {
got, err := ss.Search(ctx, SearchParams{
Resource: "targets",
+1 -1
View File
@@ -125,7 +125,7 @@ func (s *StatusService) Status(ctx context.Context) (*Status, error) {
us.AuthTokens = append(us.AuthTokens, *ts)
}
for _, rt := range []resourceType{targetResourceType, sessionResourceType} {
for _, rt := range []resourceType{aliasResourceType, targetResourceType, sessionResourceType} {
ts, err := s.resourceStatus(ctx, u, rt)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
+39 -9
View File
@@ -10,6 +10,7 @@ import (
"testing"
"time"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
@@ -121,6 +122,10 @@ func TestStatus(t *testing.T) {
},
},
Resources: []ResourceStatus{
{
Name: string(aliasResourceType),
Count: 0,
},
{
Name: string(targetResourceType),
Count: 0,
@@ -145,6 +150,10 @@ func TestStatus(t *testing.T) {
},
},
Resources: []ResourceStatus{
{
Name: string(aliasResourceType),
Count: 0,
},
{
Name: string(targetResourceType),
Count: 0,
@@ -165,7 +174,7 @@ func TestStatus(t *testing.T) {
got, err := ss.Status(ctx)
assert.NoError(t, err)
lastErr := got.Users[0].Resources[0].LastError
lastErr := got.Users[0].Resources[1].LastError
assert.NotNil(t, lastErr)
assert.Equal(t, "test error", lastErr.Error)
assert.NotZero(t, lastErr.LastReturned)
@@ -197,6 +206,15 @@ func TestStatus(t *testing.T) {
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{sess}, [][]string{nil})))
require.NoError(t, err)
als := []*aliases.Alias{
alias("1"),
alias("2"),
alias("3"),
}
err = r.refreshAliases(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*aliases.Alias{als}, [][]string{nil})))
require.NoError(t, err)
got, err := ss.Status(ctx)
assert.NoError(t, err)
@@ -212,19 +230,19 @@ func TestStatus(t *testing.T) {
assert.Equal(t, Map(got.Users[0].Resources, func(i ResourceStatus) string {
return i.Name
}), []string{string(targetResourceType), string(sessionResourceType)})
}), []string{string(aliasResourceType), string(targetResourceType), string(sessionResourceType)})
assert.Equal(t, Map(got.Users[0].Resources, func(i ResourceStatus) int {
return i.Count
}), []int{4, 3})
}), []int{3, 4, 3})
assert.Equal(t, Map(got.Users[0].Resources, func(i ResourceStatus) bool {
return i.LastError == nil
}), []bool{false, true}, "expected an error for target resource and none for session resource")
}), []bool{true, false, true}, "expected an error for target resource and none for other resources")
assert.Equal(t, Map(got.Users[0].Resources, func(i ResourceStatus) bool {
return i.RefreshToken == nil
}), []bool{false, false})
}), []bool{false, false, false})
// User 2 status
assert.Equal(t, Map(got.Users[1].AuthTokens, func(i AuthTokenStatus) string {
@@ -233,19 +251,19 @@ func TestStatus(t *testing.T) {
assert.Equal(t, Map(got.Users[1].Resources, func(i ResourceStatus) string {
return i.Name
}), []string{string(targetResourceType), string(sessionResourceType)})
}), []string{string(aliasResourceType), string(targetResourceType), string(sessionResourceType)})
assert.Equal(t, Map(got.Users[1].Resources, func(i ResourceStatus) int {
return i.Count
}), []int{2, 0})
}), []int{0, 2, 0})
assert.Equal(t, Map(got.Users[1].Resources, func(i ResourceStatus) bool {
return i.LastError == nil
}), []bool{true, true})
}), []bool{true, true, true})
assert.Equal(t, Map(got.Users[1].Resources, func(i ResourceStatus) bool {
return i.RefreshToken == nil
}), []bool{false, true}, "targets expected to have a refresh token and sessions aren't")
}), []bool{true, false, true}, "targets expected to have a refresh token and others aren't")
})
}
@@ -289,10 +307,18 @@ func TestStatus_unsupported(t *testing.T) {
AuthTokenId: at1.Id,
}))
err = r.refreshAliases(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithAliasRetrievalFunc(testNoRefreshRetrievalFunc[*aliases.Alias](t)))
require.ErrorIs(t, err, ErrRefreshNotSupported)
err = r.refreshTargets(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithTargetRetrievalFunc(testNoRefreshRetrievalFunc[*targets.Target](t)))
require.ErrorIs(t, err, ErrRefreshNotSupported)
err = r.refreshSessions(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
WithSessionRetrievalFunc(testNoRefreshRetrievalFunc[*sessions.Session](t)))
require.ErrorIs(t, err, ErrRefreshNotSupported)
got, err := ss.Status(ctx)
assert.NoError(t, err)
@@ -315,6 +341,10 @@ func TestStatus_unsupported(t *testing.T) {
},
},
Resources: []ResourceStatus{
{
Name: string(aliasResourceType),
Count: 0,
},
{
Name: string(targetResourceType),
Count: 0,
@@ -11,6 +11,7 @@ import (
"strconv"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
@@ -22,6 +23,7 @@ import (
// SearchResult is the struct returned to search requests.
type SearchResult struct {
Aliases []*aliases.Alias `json:"aliases,omitempty"`
Targets []*targets.Target `json:"targets,omitempty"`
Sessions []*sessions.Session `json:"sessions,omitempty"`
}
@@ -147,6 +149,7 @@ func newSearchHandlerFunc(ctx context.Context, repo *cache.Repository, refreshSe
// toApiResult converts a domain search result to an api search result
func toApiResult(sr *cache.SearchResult) *SearchResult {
return &SearchResult{
Aliases: sr.Aliases,
Targets: sr.Targets,
Sessions: sr.Sessions,
}
@@ -9,6 +9,7 @@ import (
"sync"
"testing"
"github.com/hashicorp/boundary/api/aliases"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/api/sessions"
"github.com/hashicorp/boundary/api/targets"
@@ -78,14 +79,21 @@ func (s *TestServer) AddKeyringToken(t *testing.T, address, keyring, tokenName,
}))
}
// AddResources adds targets to the cache for the provided address, token name,
// and keyring type. They token info must already be known to the server.
func (s *TestServer) AddResources(t *testing.T, p *authtokens.AuthToken, tars []*targets.Target, sess []*sessions.Session, atReadFn cache.BoundaryTokenReaderFn) {
// AddResources adds targets, sessions, and aliases to the cache for the
// provided address, token name, and keyring type. They token info must already
// be known to the server.
func (s *TestServer) AddResources(t *testing.T, p *authtokens.AuthToken, alts []*aliases.Alias, tars []*targets.Target, sess []*sessions.Session, atReadFn cache.BoundaryTokenReaderFn) {
t.Helper()
ctx := context.Background()
r, err := cache.NewRepository(ctx, s.CacheServer.store.Load(), &sync.Map{}, s.cmd.ReadTokenFromKeyring, atReadFn)
require.NoError(t, err)
altFn := func(ctx context.Context, _, tok string, _ cache.RefreshTokenValue) ([]*aliases.Alias, []string, cache.RefreshTokenValue, error) {
if tok != p.Token {
return nil, nil, "", nil
}
return alts, nil, "addedaliases", nil
}
tarFn := func(ctx context.Context, _, tok string, _ cache.RefreshTokenValue) ([]*targets.Target, []string, cache.RefreshTokenValue, error) {
if tok != p.Token {
return nil, nil, "", nil
@@ -100,7 +108,7 @@ func (s *TestServer) AddResources(t *testing.T, p *authtokens.AuthToken, tars []
}
rs, err := cache.NewRefreshService(ctx, r, hclog.NewNullLogger(), 0, 0)
require.NoError(t, err)
require.NoError(t, rs.Refresh(ctx, cache.WithTargetRetrievalFunc(tarFn), cache.WithSessionRetrievalFunc(sessFn)))
require.NoError(t, rs.Refresh(ctx, cache.WithAliasRetrievalFunc(altFn), cache.WithTargetRetrievalFunc(tarFn), cache.WithSessionRetrievalFunc(sessFn)))
}
// AddUnsupportedCachingData provides data in a way that simulates it coming from
+24 -1
View File
@@ -17,12 +17,13 @@ create table if not exists user (
create table if not exists resource_type_enm(
string text not null primary key
constraint only_predefined_resource_types_allowed
check(string in ('unknown', 'target', 'session'))
check(string in ('unknown', 'alias', 'target', 'session'))
);
insert into resource_type_enm (string)
values
('unknown'),
('alias'),
('target'),
('session');
@@ -155,6 +156,28 @@ create table if not exists session (
primary key (fk_user_id, id)
);
-- alias contains cached boundary alias resource for a specific user and
-- with specific fields extracted to facilitate searching over those fields
create table if not exists alias (
-- the boundary user id of the user who has was able to read/list this resource
fk_user_id text not null
references user(id)
on delete cascade,
-- the resource id from boundary of this session
id text not null
check (length(id) > 0),
-- the following fields are used for searching and are set to the values
-- from the boundary resource
type text,
scope_id text,
destination_id text,
value text,
-- item is the json representation of this resource from the perspective of
-- of the user whose id is set in fk_user_id
item text,
primary key (fk_user_id, id)
);
-- contains errors from the last attempt to sync data from boundary for a
-- specific resource type
create table if not exists api_error (