Files
threshold/internal/clientcache/internal/cache/refresh_test.go
T
Jim fba9d1cdf1 Add client daemon repo, server, and cmd
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
2024-01-05 07:43:20 -08:00

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",
}
}