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:
1 parent
2e55ae72ba
commit
a2ea1f8f90
16 files changed
+1359
-18
No files matched your search
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in new issue
Block a user