From a2ea1f8f90e36d47920d0e8697387ffda75dc0c4 Mon Sep 17 00:00:00 2001 From: Todd Date: Tue, 27 Feb 2024 13:10:20 -0800 Subject: [PATCH] Add aliases to the client cache daemon and make them searchable (#4418) * Add aliases to the client cache daemon and make them searchable --- internal/clientcache/cmd/search/search.go | 72 +++ .../clientcache/cmd/search/search_test.go | 6 +- .../clientcache/internal/cache/options.go | 9 + .../internal/cache/options_test.go | 14 + .../clientcache/internal/cache/refresh.go | 20 + .../internal/cache/refresh_test.go | 214 ++++++++ .../internal/cache/repository_aliases.go | 373 ++++++++++++++ .../internal/cache/repository_aliases_test.go | 480 ++++++++++++++++++ .../cache/repository_refresh_token.go | 3 +- internal/clientcache/internal/cache/search.go | 14 +- .../clientcache/internal/cache/search_test.go | 78 +++ internal/clientcache/internal/cache/status.go | 2 +- .../clientcache/internal/cache/status_test.go | 48 +- .../internal/daemon/search_handler.go | 3 + .../clientcache/internal/daemon/testing.go | 16 +- internal/clientcache/internal/db/schema.sql | 25 +- 16 files changed, 1359 insertions(+), 18 deletions(-) create mode 100644 internal/clientcache/internal/cache/repository_aliases.go create mode 100644 internal/clientcache/internal/cache/repository_aliases_test.go diff --git a/internal/clientcache/cmd/search/search.go b/internal/clientcache/cmd/search/search.go index b8ba96d1c..0693b35f0 100644 --- a/internal/clientcache/cmd/search/search.go +++ b/internal/clientcache/cmd/search/search.go @@ -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" diff --git a/internal/clientcache/cmd/search/search_test.go b/internal/clientcache/cmd/search/search_test.go index 875d5c020..45ad346b5 100644 --- a/internal/clientcache/cmd/search/search_test.go +++ b/internal/clientcache/cmd/search/search_test.go @@ -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{ diff --git a/internal/clientcache/internal/cache/options.go b/internal/clientcache/internal/cache/options.go index 574d39e09..dd0f32dea 100644 --- a/internal/clientcache/internal/cache/options.go +++ b/internal/clientcache/internal/cache/options.go @@ -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 { diff --git a/internal/clientcache/internal/cache/options_test.go b/internal/clientcache/internal/cache/options_test.go index a53d77276..7c0a7e7fa 100644 --- a/internal/clientcache/internal/cache/options_test.go +++ b/internal/clientcache/internal/cache/options_test.go @@ -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) diff --git a/internal/clientcache/internal/cache/refresh.go b/internal/clientcache/internal/cache/refresh.go index d8a6bd143..762e538fe 100644 --- a/internal/clientcache/internal/cache/refresh.go +++ b/internal/clientcache/internal/cache/refresh.go @@ -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 diff --git a/internal/clientcache/internal/cache/refresh_test.go b/internal/clientcache/internal/cache/refresh_test.go index 588af254b..f86d8d69e 100644 --- a/internal/clientcache/internal/cache/refresh_test.go +++ b/internal/clientcache/internal/cache/refresh_test.go @@ -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), + } +} diff --git a/internal/clientcache/internal/cache/repository_aliases.go b/internal/clientcache/internal/cache/repository_aliases.go new file mode 100644 index 000000000..5f5ddb215 --- /dev/null +++ b/internal/clientcache/internal/cache/repository_aliases.go @@ -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" +} diff --git a/internal/clientcache/internal/cache/repository_aliases_test.go b/internal/clientcache/internal/cache/repository_aliases_test.go new file mode 100644 index 000000000..0763a1b29 --- /dev/null +++ b/internal/clientcache/internal/cache/repository_aliases_test.go @@ -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) +} diff --git a/internal/clientcache/internal/cache/repository_refresh_token.go b/internal/clientcache/internal/cache/repository_refresh_token.go index ccc95ec0c..c773cca03 100644 --- a/internal/clientcache/internal/cache/repository_refresh_token.go +++ b/internal/clientcache/internal/cache/repository_refresh_token.go @@ -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 diff --git a/internal/clientcache/internal/cache/search.go b/internal/clientcache/internal/cache/search.go index ebc37e511..b6e2837be 100644 --- a/internal/clientcache/internal/cache/search.go +++ b/internal/clientcache/internal/cache/search.go @@ -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, diff --git a/internal/clientcache/internal/cache/search_test.go b/internal/clientcache/internal/cache/search_test.go index bb5fa4550..2de224bc5 100644 --- a/internal/clientcache/internal/cache/search_test.go +++ b/internal/clientcache/internal/cache/search_test.go @@ -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", diff --git a/internal/clientcache/internal/cache/status.go b/internal/clientcache/internal/cache/status.go index b42383219..e51224a1c 100644 --- a/internal/clientcache/internal/cache/status.go +++ b/internal/clientcache/internal/cache/status.go @@ -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) diff --git a/internal/clientcache/internal/cache/status_test.go b/internal/clientcache/internal/cache/status_test.go index cf5039fe1..f18a5b089 100644 --- a/internal/clientcache/internal/cache/status_test.go +++ b/internal/clientcache/internal/cache/status_test.go @@ -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, diff --git a/internal/clientcache/internal/daemon/search_handler.go b/internal/clientcache/internal/daemon/search_handler.go index 4597c3b7d..be3bcaf46 100644 --- a/internal/clientcache/internal/daemon/search_handler.go +++ b/internal/clientcache/internal/daemon/search_handler.go @@ -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, } diff --git a/internal/clientcache/internal/daemon/testing.go b/internal/clientcache/internal/daemon/testing.go index f7db5c001..e4b61fc3b 100644 --- a/internal/clientcache/internal/daemon/testing.go +++ b/internal/clientcache/internal/daemon/testing.go @@ -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 diff --git a/internal/clientcache/internal/db/schema.sql b/internal/clientcache/internal/db/schema.sql index 5372ee7ab..6ec26401b 100644 --- a/internal/clientcache/internal/db/schema.sql +++ b/internal/clientcache/internal/db/schema.sql @@ -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 (