The commands added are: boundary daemon start boundary daemon stop boundary daemon add-token boundary daemon status boundary search If these actions are performed on a platform that doesn't support sqlite natively in go an error is returned. Besides some hooks into the cmd package all the significant logic resides in internal/clientcache feat (cmds/authenticate): output the token name when storing token feat: update go-dbw which now uses pure go sqlite driver refact (base.Command): export FlagOutputCurlString Exporting FlagOutputCurlString allows other commands that embed the base.Command to access the flag as needed feat: add a cache Repository to manage cache storage feat: add a "cache" CLI command This new command will support both managing and searching the local CLI cache daemon. feat: start cache daemon as needed when issue CLI commands Move files to new directories and add persona table (#3526) * Add persona table and move files to new directories Client daemon now uses unix sockets and update pathing (#3535) * Add unix socket listen and dialers with file permissions applied * Update the pathing to match RFC Cache refreshing now uses all stored personas. (#3545) Updating headers for LLB files Decouple daemon and cache server (#3566) * Separate daemon from server * Add persona handler to daemon server (#3579) Moved search to use mql and added tests for the search command (#3589) * search uses hashicorp/mql Windows support for client side daemon (#3618) * exec.Command for background process * Log to stderr when in foreground and file always * Use pid files across platform for start and stop Requests with version info must match the daemon's version (#3640) * requests with version info must match daemons Update dependencies and breakout cgo/pure go sqlite drivers (#3657) upgrade go-dbw deps and add cgo build tags Enable graceful shutdown in windows (#3695) * Enable graceful shutdown in windows Co-authored-by: Jim <jlambert@hashicorp.com> Targets delete when a persona changes address or user id (#3673) * Handle cache refresh in repo Add sessions to the client side cache (#3682) * Add sessions to repository * Search command supports sessions * adding session related tests to db and refresh Change cache schema from personas to tokens+users (#3737) * Change from personas to tokens+users * Separate keyring tokens from auth tokens Add support for keyringless auth tokens (#3765) * Add support for keyringless auth tokens Remove expired auth tokens from cache (#3768) * Remove expired auth tokens from cache Complete the TODO of checking for inflight tx (#3779) Delete tokens that the controller says are invalid (#3782) fix search test with boundary token check (#3799) Collect resource info in repository_<resource>s.go (#3769) * Collect resource info in repository_<resource>s.go * Move search logic into the domain (#3794) Daemon and search restricted to supported platforms (#3801) * Daemon and search restricted to supported platforms Reorganize the clientcache packages (#3814) * Refactor for self contained domain logic * Put clientcache stuff under internal
418 lines
13 KiB
Go
418 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/api"
|
|
"github.com/hashicorp/boundary/api/authtokens"
|
|
"github.com/hashicorp/boundary/api/sessions"
|
|
"github.com/hashicorp/boundary/api/targets"
|
|
"github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
// testStaticResourceRetrievalFunc returns a function that always returns the
|
|
// provided slice and a nil error. The returned function can be passed into the
|
|
// options that provide a resource retrieval func such as
|
|
// WithTargetRetrievalFunc and WithSessionRetrievalFunc.
|
|
func testStaticResourceRetrievalFunc[T any](ret []T) func(context.Context, string, string) ([]T, error) {
|
|
return func(ctx context.Context, s1, s2 string) ([]T, error) {
|
|
return ret, nil
|
|
}
|
|
}
|
|
|
|
func TestCleanAndPickTokens(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := db.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
boundaryAddr := "address"
|
|
u1 := &user{Id: "u1", Address: boundaryAddr}
|
|
at1a := &authtokens.AuthToken{
|
|
Id: "at_1a",
|
|
Token: "at_1a_token",
|
|
UserId: u1.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
at1b := &authtokens.AuthToken{
|
|
Id: "at_1b",
|
|
Token: "at_1b_token",
|
|
UserId: u1.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
|
|
keyringOnlyUser := &user{Id: "keyringUser", Address: boundaryAddr}
|
|
keyringAuthToken1 := &authtokens.AuthToken{
|
|
Id: "at_2a",
|
|
Token: "at_2a_token",
|
|
UserId: keyringOnlyUser.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
keyringAuthToken2 := &authtokens.AuthToken{
|
|
Id: "at_2b",
|
|
Token: "at_2b_token",
|
|
UserId: keyringOnlyUser.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at1a, keyringAuthToken1, at1b, keyringAuthToken2}
|
|
unauthorizedAuthTokens := []*authtokens.AuthToken{}
|
|
randomErrorAuthTokens := []*authtokens.AuthToken{}
|
|
fakeBoundaryLookupFn := func(ctx context.Context, addr, at string) (*authtokens.AuthToken, error) {
|
|
for _, v := range randomErrorAuthTokens {
|
|
if at == v.Token {
|
|
return nil, errors.New("test error")
|
|
}
|
|
}
|
|
for _, v := range unauthorizedAuthTokens {
|
|
if at == v.Token {
|
|
return nil, api.ErrUnauthorized
|
|
}
|
|
}
|
|
for _, v := range boundaryAuthTokens {
|
|
if at == v.Token {
|
|
return v, nil
|
|
}
|
|
}
|
|
return nil, errors.New("not found")
|
|
}
|
|
|
|
atMap := make(map[ringToken]*authtokens.AuthToken)
|
|
r, err := NewRepository(ctx, s, &sync.Map{},
|
|
mapBasedAuthTokenKeyringLookup(atMap),
|
|
fakeBoundaryLookupFn)
|
|
require.NoError(t, err)
|
|
rs, err := NewRefreshService(ctx, r)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("unknown user", func(t *testing.T) {
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, &user{Id: "unknownuser", Address: "unknown"})
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, got)
|
|
})
|
|
|
|
t.Run("both memory and keyring stored token", func(t *testing.T) {
|
|
key := ringToken{"k1", "t1"}
|
|
atMap[key] = at1a
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key.k,
|
|
TokenName: key.t,
|
|
AuthTokenId: at1a.Id,
|
|
}))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
|
|
// delete the keyringToken from the keyring and see it get removed from the response
|
|
delete(atMap, key)
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1b.Token})
|
|
})
|
|
|
|
t.Run("2 memory tokens", func(t *testing.T) {
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1a.Token))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
})
|
|
|
|
t.Run("boundary in memory auth token expires", func(t *testing.T) {
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1a.Token))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
|
|
t.Cleanup(func() {
|
|
unauthorizedAuthTokens = nil
|
|
})
|
|
|
|
unauthorizedAuthTokens = []*authtokens.AuthToken{at1b}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token})
|
|
})
|
|
|
|
t.Run("boundary keyring auths token expires", func(t *testing.T) {
|
|
key1 := ringToken{"k1", "t1"}
|
|
atMap[key1] = keyringAuthToken1
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key1.k,
|
|
TokenName: key1.t,
|
|
AuthTokenId: keyringAuthToken1.Id,
|
|
}))
|
|
key2 := ringToken{"k2", "t2"}
|
|
atMap[key2] = keyringAuthToken2
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key2.k,
|
|
TokenName: key2.t,
|
|
AuthTokenId: keyringAuthToken2.Id,
|
|
}))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token, keyringAuthToken2.Token})
|
|
|
|
t.Cleanup(func() {
|
|
unauthorizedAuthTokens = nil
|
|
})
|
|
|
|
unauthorizedAuthTokens = []*authtokens.AuthToken{keyringAuthToken2}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token})
|
|
})
|
|
|
|
t.Run("boundary in memory auth token check errors", func(t *testing.T) {
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1a.Token))
|
|
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token, at1b.Token})
|
|
|
|
t.Cleanup(func() {
|
|
randomErrorAuthTokens = nil
|
|
})
|
|
|
|
randomErrorAuthTokens = []*authtokens.AuthToken{at1b}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, u1)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{at1a.Token})
|
|
})
|
|
|
|
t.Run("boundary keyring auths token check errors", func(t *testing.T) {
|
|
key1 := ringToken{"k1", "t1"}
|
|
atMap[key1] = keyringAuthToken1
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key1.k,
|
|
TokenName: key1.t,
|
|
AuthTokenId: keyringAuthToken1.Id,
|
|
}))
|
|
key2 := ringToken{"k2", "t2"}
|
|
atMap[key2] = keyringAuthToken2
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key2.k,
|
|
TokenName: key2.t,
|
|
AuthTokenId: keyringAuthToken2.Id,
|
|
}))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token, keyringAuthToken2.Token})
|
|
|
|
t.Cleanup(func() {
|
|
randomErrorAuthTokens = nil
|
|
})
|
|
|
|
randomErrorAuthTokens = []*authtokens.AuthToken{keyringAuthToken2}
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token})
|
|
})
|
|
|
|
t.Run("2 keyring tokens", func(t *testing.T) {
|
|
key1 := ringToken{"k1", "t1"}
|
|
atMap[key1] = keyringAuthToken1
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key1.k,
|
|
TokenName: key1.t,
|
|
AuthTokenId: keyringAuthToken1.Id,
|
|
}))
|
|
key2 := ringToken{"k2", "t2"}
|
|
atMap[key2] = keyringAuthToken2
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
|
KeyringType: key2.k,
|
|
TokenName: key2.t,
|
|
AuthTokenId: keyringAuthToken2.Id,
|
|
}))
|
|
|
|
got, err := rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, maps.Values(got), []string{keyringAuthToken1.Token, keyringAuthToken2.Token})
|
|
|
|
// Removing all keyring references and then cleaning auth tokens
|
|
// removes all auth tokens, along with the user
|
|
gotU, err := r.listUsers(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, gotU, keyringOnlyUser)
|
|
|
|
delete(atMap, key1)
|
|
delete(atMap, key2)
|
|
got, err = rs.cleanAndPickAuthTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, got)
|
|
|
|
gotT, err := r.listTokens(ctx, keyringOnlyUser)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, gotT)
|
|
gotU, err = r.listUsers(ctx)
|
|
assert.NoError(t, err)
|
|
assert.NotContains(t, gotU, keyringOnlyUser)
|
|
})
|
|
}
|
|
|
|
func TestRefresh(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := db.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
boundaryAddr := "address"
|
|
u := &user{Id: "u1", Address: boundaryAddr}
|
|
at := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: u.Id,
|
|
ExpirationTime: time.Now().Add(time.Minute),
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
|
atMap := make(map[ringToken]*authtokens.AuthToken)
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
rs, err := NewRefreshService(ctx, r)
|
|
require.NoError(t, err)
|
|
|
|
atMap[ringToken{"k", "t"}] = at
|
|
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{KeyringType: "k", TokenName: "t", AuthTokenId: at.Id}))
|
|
|
|
t.Run("set targets", func(t *testing.T) {
|
|
retTargets := []*targets.Target{
|
|
target("1"),
|
|
target("2"),
|
|
target("3"),
|
|
}
|
|
assert.NoError(t, rs.Refresh(ctx,
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil)),
|
|
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string) ([]*targets.Target, error) {
|
|
require.Equal(t, boundaryAddr, addr)
|
|
require.Equal(t, at.Token, token)
|
|
return retTargets, nil
|
|
})))
|
|
|
|
cachedTargets, err := r.ListTargets(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retTargets, cachedTargets)
|
|
|
|
t.Run("empty response clears it out", func(t *testing.T) {
|
|
assert.NoError(t, rs.Refresh(ctx,
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil)),
|
|
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string) ([]*targets.Target, error) {
|
|
require.Equal(t, boundaryAddr, addr)
|
|
require.Equal(t, at.Token, token)
|
|
return nil, nil
|
|
})))
|
|
|
|
cachedTargets, err := r.ListTargets(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, cachedTargets)
|
|
})
|
|
})
|
|
|
|
t.Run("set sessions", func(t *testing.T) {
|
|
retSess := []*sessions.Session{
|
|
session("1"),
|
|
session("2"),
|
|
session("3"),
|
|
}
|
|
assert.NoError(t, rs.Refresh(ctx,
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](nil)),
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(retSess))))
|
|
|
|
cachedSessions, err := r.ListSessions(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retSess, cachedSessions)
|
|
|
|
t.Run("empty response clears it out", func(t *testing.T) {
|
|
assert.NoError(t, rs.Refresh(ctx,
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](nil)),
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil))))
|
|
|
|
cachedTargets, err := r.ListSessions(ctx, at.Id)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, cachedTargets)
|
|
})
|
|
})
|
|
|
|
t.Run("error propogates up", func(t *testing.T) {
|
|
innerErr := errors.New("test error")
|
|
err := rs.Refresh(ctx,
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil)),
|
|
WithTargetRetrievalFunc(func(ctx context.Context, addr, token string) ([]*targets.Target, error) {
|
|
require.Equal(t, boundaryAddr, addr)
|
|
require.Equal(t, at.Token, token)
|
|
return nil, innerErr
|
|
}))
|
|
assert.ErrorContains(t, err, innerErr.Error())
|
|
err = rs.Refresh(ctx,
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](nil)),
|
|
WithSessionRetrievalFunc(func(ctx context.Context, addr, token string) ([]*sessions.Session, error) {
|
|
require.Equal(t, boundaryAddr, addr)
|
|
require.Equal(t, at.Token, token)
|
|
return nil, innerErr
|
|
}))
|
|
assert.ErrorContains(t, err, innerErr.Error())
|
|
})
|
|
|
|
t.Run("tokens that are no longer in the ring is deleted", func(t *testing.T) {
|
|
// Remove the token from the keyring, see that we can still see the
|
|
// token and then user until a Refresh happens which causes them to be
|
|
// cleaned up.
|
|
delete(atMap, ringToken{"k", "t"})
|
|
|
|
ps, err := r.listTokens(ctx, u)
|
|
require.NoError(t, err)
|
|
assert.Len(t, ps, 1)
|
|
|
|
us, err := r.listUsers(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, us, 1)
|
|
|
|
rs.Refresh(ctx,
|
|
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](nil)),
|
|
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](nil)))
|
|
|
|
ps, err = r.listTokens(ctx, u)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, ps)
|
|
|
|
// And since the last token was deleted, the user also was deleted
|
|
us, err = r.listUsers(ctx)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, us)
|
|
})
|
|
}
|
|
|
|
func target(suffix string) *targets.Target {
|
|
return &targets.Target{
|
|
Id: fmt.Sprintf("target_%s", suffix),
|
|
Name: fmt.Sprintf("name_%s", suffix),
|
|
Description: fmt.Sprintf("description_%s", suffix),
|
|
Address: fmt.Sprintf("address_%s", suffix),
|
|
Type: "tcp",
|
|
}
|
|
}
|
|
|
|
func session(suffix string) *sessions.Session {
|
|
return &sessions.Session{
|
|
Id: fmt.Sprintf("session_%s", suffix),
|
|
Type: "tcp",
|
|
}
|
|
}
|