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
This commit is contained in:
1 parent
6ba3676c37
commit
fba9d1cdf1
71 files changed
+8980
-972
No files matched your search
+38
-12
@@ -16,6 +16,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -57,7 +58,12 @@ type Config struct {
|
||||
// HttpClient.
|
||||
Addr string
|
||||
|
||||
// Token is the client token that reuslts from authentication and can be
|
||||
// Set when the Addr is a unix socket. The parsing of this field takes
|
||||
// a filename approach instead of a URL approach since parsing file names
|
||||
// as a URL may error out on some platforms.
|
||||
socketAddr string
|
||||
|
||||
// Token is the client token that results from authentication and can be
|
||||
// used to make calls into Boundary
|
||||
Token string
|
||||
|
||||
@@ -254,6 +260,16 @@ func (c *Config) ConfigureTLS() error {
|
||||
// This also removes any trailing "/v1"; we'll use that in our commands so we
|
||||
// don't require it from users.
|
||||
func (c *Config) setAddr(addr string) error {
|
||||
if strings.HasPrefix(addr, "unix://") {
|
||||
c.socketAddr = strings.TrimPrefix(addr, "unix://")
|
||||
c.socketAddr = filepath.Clean(c.socketAddr)
|
||||
|
||||
// Update the specified address to the cleaned up version
|
||||
c.Addr = fmt.Sprintf("unix://%s", c.socketAddr)
|
||||
return nil
|
||||
}
|
||||
c.socketAddr = ""
|
||||
|
||||
u, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing address: %w", err)
|
||||
@@ -582,6 +598,7 @@ func (c *Client) Clone() *Client {
|
||||
config := c.config
|
||||
|
||||
newConfig := &Config{
|
||||
socketAddr: config.socketAddr,
|
||||
Addr: config.Addr,
|
||||
Token: config.Token,
|
||||
RecoveryKmsWrapper: config.RecoveryKmsWrapper,
|
||||
@@ -645,31 +662,29 @@ func (c *Client) NewRequest(ctx context.Context, method, requestPath string, bod
|
||||
}
|
||||
|
||||
c.modifyLock.RLock()
|
||||
addr := c.config.Addr
|
||||
srvLookup := c.config.SRVLookup
|
||||
token := c.config.Token
|
||||
httpClient := c.config.HttpClient
|
||||
headers := copyHeaders(c.config.Headers)
|
||||
c.modifyLock.RUnlock()
|
||||
|
||||
u, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(addr, "unix://") {
|
||||
socket := strings.TrimPrefix(addr, "unix://")
|
||||
var hostHeader string
|
||||
var u *url.URL
|
||||
switch {
|
||||
case c.config.socketAddr != "":
|
||||
transport := httpClient.Transport.(*http.Transport)
|
||||
transport.DialContext = func(context.Context, string, string) (net.Conn, error) {
|
||||
dialer := net.Dialer{}
|
||||
return dialer.DialContext(ctx, "unix", socket)
|
||||
return dialer.DialContext(ctx, "unix", c.config.socketAddr)
|
||||
}
|
||||
|
||||
// Since the address points to a unix domain socket, the scheme in the
|
||||
// *URL would be set to `unix`. The *URL in the client is expected to
|
||||
// be pointing to the protocol used in the application layer and not to
|
||||
// the transport layer. Hence, setting the fields accordingly.
|
||||
u = &url.URL{}
|
||||
u.Scheme = "http"
|
||||
u.Host = c.config.socketAddr
|
||||
u.Path = ""
|
||||
|
||||
// Go 1.21.0 introduced strict host header validation for clients.
|
||||
@@ -680,7 +695,17 @@ func (c *Client) NewRequest(ctx context.Context, method, requestPath string, bod
|
||||
// that isn't the actual unix domain socket address. Following
|
||||
// Docker's lead (https://github.com/moby/moby/pull/45942),
|
||||
// use a localhost TLD.
|
||||
u.Host = "api.boundary.localhost"
|
||||
hostHeader = "api.boundary.localhost"
|
||||
case c.config.Addr != "":
|
||||
addr := c.config.Addr
|
||||
var err error
|
||||
u, err = url.Parse(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostHeader = u.Host
|
||||
default:
|
||||
return nil, errors.New("no valid address discovered")
|
||||
}
|
||||
|
||||
host := u.Host
|
||||
@@ -707,8 +732,9 @@ func (c *Client) NewRequest(ctx context.Context, method, requestPath string, bod
|
||||
Host: host,
|
||||
Path: path.Join(u.Path, "/v1/", requestPath),
|
||||
},
|
||||
Host: u.Host,
|
||||
Host: hostHeader,
|
||||
}
|
||||
|
||||
req.Header = headers
|
||||
req.Header.Set("authorization", "Bearer "+token)
|
||||
req.Header.Set("content-type", "application/json")
|
||||
|
||||
+48
-39
@@ -11,60 +11,70 @@ import (
|
||||
|
||||
func TestConfigSetAddress(t *testing.T) {
|
||||
type test struct {
|
||||
name string
|
||||
input string
|
||||
address string
|
||||
err string
|
||||
name string
|
||||
input string
|
||||
address string
|
||||
socketAddr string
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
{
|
||||
"bare",
|
||||
"http://127.0.0.1:9200",
|
||||
"http://127.0.0.1:9200",
|
||||
"",
|
||||
name: "bare",
|
||||
input: "http://127.0.0.1:9200",
|
||||
address: "http://127.0.0.1:9200",
|
||||
},
|
||||
{
|
||||
"bare with version",
|
||||
"http://127.0.0.1:9200/v1",
|
||||
"http://127.0.0.1:9200",
|
||||
"",
|
||||
name: "bare with version",
|
||||
input: "http://127.0.0.1:9200/v1",
|
||||
address: "http://127.0.0.1:9200",
|
||||
},
|
||||
{
|
||||
"bare with version and trailing slash",
|
||||
"http://127.0.0.1:9200/v1/",
|
||||
"http://127.0.0.1:9200",
|
||||
"",
|
||||
name: "bare with version and trailing slash",
|
||||
input: "http://127.0.0.1:9200/v1/",
|
||||
address: "http://127.0.0.1:9200",
|
||||
},
|
||||
{
|
||||
"valid top level scope",
|
||||
"http://127.0.0.1:9200/v1/scopes",
|
||||
"http://127.0.0.1:9200",
|
||||
"",
|
||||
name: "valid top level scope",
|
||||
input: "http://127.0.0.1:9200/v1/scopes",
|
||||
address: "http://127.0.0.1:9200",
|
||||
},
|
||||
{
|
||||
"valid scope",
|
||||
"http://127.0.0.1:9200/v1/scopes/scopeid",
|
||||
"http://127.0.0.1:9200",
|
||||
"",
|
||||
name: "valid scope",
|
||||
input: "http://127.0.0.1:9200/v1/scopes/scopeid",
|
||||
address: "http://127.0.0.1:9200",
|
||||
},
|
||||
{
|
||||
"longer path project",
|
||||
"http://127.0.0.1:9200/v1/auth-methods",
|
||||
"http://127.0.0.1:9200",
|
||||
"",
|
||||
name: "longer path project",
|
||||
input: "http://127.0.0.1:9200/v1/auth-methods",
|
||||
address: "http://127.0.0.1:9200",
|
||||
},
|
||||
{
|
||||
"valid project",
|
||||
"http://127.0.0.1:9200/my-install",
|
||||
"http://127.0.0.1:9200/my-install",
|
||||
"",
|
||||
name: "valid project",
|
||||
input: "http://127.0.0.1:9200/my-install",
|
||||
address: "http://127.0.0.1:9200/my-install",
|
||||
},
|
||||
{
|
||||
"valid project path containing v1",
|
||||
"http://127.0.0.1:9200/randomPathHasv1InIt",
|
||||
"http://127.0.0.1:9200/randomPathHasv1InIt",
|
||||
"",
|
||||
name: "valid project path containing v1",
|
||||
input: "http://127.0.0.1:9200/randomPathHasv1InIt",
|
||||
address: "http://127.0.0.1:9200/randomPathHasv1InIt",
|
||||
},
|
||||
{
|
||||
name: "unix socket with linux pathing",
|
||||
input: "unix:///home/username/.boundary/socketdir/boundary.sock",
|
||||
address: "unix:///home/username/.boundary/socketdir/boundary.sock",
|
||||
socketAddr: "/home/username/.boundary/socketdir/boundary.sock",
|
||||
},
|
||||
{
|
||||
name: "unix socket with weird linux pathing",
|
||||
input: "unix:///home/username/.boundary/something/../socketdir/boundary.sock",
|
||||
address: "unix:///home/username/.boundary/socketdir/boundary.sock",
|
||||
socketAddr: "/home/username/.boundary/socketdir/boundary.sock",
|
||||
},
|
||||
{
|
||||
name: "unix socket with windows pathing",
|
||||
input: "unix://C:\\Users\\Admin\\AppData\\boundary.sock",
|
||||
address: "unix://C:\\Users\\Admin\\AppData\\boundary.sock",
|
||||
socketAddr: "C:\\Users\\Admin\\AppData\\boundary.sock",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -72,10 +82,9 @@ func TestConfigSetAddress(t *testing.T) {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
var c Config
|
||||
err := c.setAddr(v.input)
|
||||
if err != nil {
|
||||
assert.Equal(t, v.err, err.Error())
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v.address, c.Addr)
|
||||
assert.Equal(t, v.socketAddr, c.socketAddr)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -88,8 +88,9 @@ require github.com/hashicorp/go-dbw v0.1.1
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
github.com/cenkalti/backoff/v4 v4.2.1
|
||||
github.com/creack/pty v1.1.20
|
||||
github.com/cenkalti/backoff/v4 v4.2.0
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/glebarez/sqlite v1.9.0
|
||||
github.com/golang/protobuf v1.5.3
|
||||
github.com/hashicorp/cap/ldap v0.0.0-20231012003312-273118a6e3b8
|
||||
github.com/hashicorp/dbassert v0.0.0-20231012105025-1bc1bd88e22b
|
||||
@@ -102,12 +103,30 @@ require (
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/miekg/dns v1.1.56
|
||||
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/sevlyar/go-daemon v0.1.6
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
golang.org/x/net v0.17.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
gorm.io/driver/sqlite v1.5.4 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2 // indirect
|
||||
@@ -148,6 +167,7 @@ require (
|
||||
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.5 // indirect
|
||||
github.com/hashicorp/go-plugin v1.5.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3 // indirect
|
||||
github.com/hashicorp/mql v0.0.0-20230814234610-74af33a21d7f
|
||||
github.com/hashicorp/vault/sdk v0.3.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
@@ -160,16 +180,14 @@ require (
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jefferai/go-libsecret v0.0.0-20210525195240-b53481abef97 // indirect
|
||||
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f // indirect
|
||||
github.com/jinzhu/gorm v1.9.12 // indirect
|
||||
github.com/jinzhu/gorm v1.9.16 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
@@ -202,5 +220,4 @@ require (
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/sqlite v1.5.4 // indirect
|
||||
)
|
||||
@@ -26,6 +26,7 @@ github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nB
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -34,6 +35,7 @@ github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVK
|
||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0=
|
||||
github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
|
||||
@@ -61,8 +63,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
|
||||
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
@@ -90,8 +92,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4=
|
||||
github.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
|
||||
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
|
||||
@@ -113,6 +115,8 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -138,6 +142,10 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
|
||||
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
@@ -152,7 +160,7 @@ github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8V
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@@ -205,6 +213,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
@@ -311,6 +321,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/mql v0.0.0-20230814234610-74af33a21d7f h1:hpvyk8Al2vfAtoqDOPJg9Z8WKMfpYcsDG85TEELbgmM=
|
||||
github.com/hashicorp/mql v0.0.0-20230814234610-74af33a21d7f/go.mod h1:pSghNOmPNBD9/Bvx0M4EC2JB/mEVA6qlB9IKpNCiqOA=
|
||||
github.com/hashicorp/nodeenrollment v0.2.9 h1:pFdMjd6VqDpqWtzr2+p//7DHKKPwxmbNoENCKCOYuvs=
|
||||
github.com/hashicorp/nodeenrollment v0.2.9/go.mod h1:7lR/2H0cHO9ZsEQj7ixoaPYB7vNu5yP+8jyoCZtHeAY=
|
||||
github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ=
|
||||
@@ -396,8 +408,8 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/jimlambrt/gldap v0.1.9 h1:OPIRGQ/zdjKNLZYgLhNq1B6kMSB0aFmfgssWsOO0Brw=
|
||||
github.com/jimlambrt/gldap v0.1.9/go.mod h1:wQXacI2If7+C8z/IaTIf6Sbb+tqgFoqzujN2AaGzyck=
|
||||
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
|
||||
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
|
||||
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
|
||||
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
@@ -408,6 +420,8 @@ github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgb
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
@@ -455,6 +469,7 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@@ -558,6 +573,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@@ -578,6 +596,8 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sethvargo/go-diceware v0.3.0 h1:UVVEfmN/uF50JfWAN7nbY6CiAlp5xeSx+5U0lWKkMCQ=
|
||||
github.com/sethvargo/go-diceware v0.3.0/go.mod h1:lH5Q/oSPMivseNdhMERAC7Ti5oOPqsaVddU1BcN1CY0=
|
||||
github.com/sevlyar/go-daemon v0.1.6 h1:EUh1MDjEM4BI109Jign0EaknA2izkOyi0LV3ro3QQGs=
|
||||
github.com/sevlyar/go-daemon v0.1.6/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
@@ -707,6 +727,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -719,7 +740,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -920,6 +943,14 @@ gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48=
|
||||
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
|
||||
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/version"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*AddTokenCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*AddTokenCommand)(nil)
|
||||
)
|
||||
|
||||
type AddTokenCommand struct {
|
||||
*base.Command
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Synopsis() string {
|
||||
return "Add an auth token to a running boundary daemon"
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary daemon add-token [options]
|
||||
|
||||
Add an auth token to the daemon:
|
||||
|
||||
$ boundary daemon add-token
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetClient | base.FlagSetOutputFormat)
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Run(args []string) int {
|
||||
ctx := c.Context
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
apiErr, err := c.Add(ctx)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
if apiErr != nil {
|
||||
c.PrintApiError(apiErr, "Error from daemon when adding a token")
|
||||
return base.CommandApiError
|
||||
|
||||
}
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Add(ctx context.Context) (*api.Error, error) {
|
||||
const op = "daemon.(AddTokenCommand).Add"
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyringType, tokenName, err := c.DiscoverKeyringTokenInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pa := daemon.UpsertTokenRequest{
|
||||
BoundaryAddr: client.Addr(),
|
||||
}
|
||||
switch keyringType {
|
||||
case "", base.NoneKeyring:
|
||||
keyringType = base.NoneKeyring
|
||||
token := client.Token()
|
||||
if parts := strings.SplitN(token, "_", 4); len(parts) == 3 {
|
||||
pa.AuthTokenId = strings.Join(parts[:2], "_")
|
||||
} else {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "found auth token is not in the proper format")
|
||||
}
|
||||
pa.AuthToken = token
|
||||
default:
|
||||
at := c.ReadTokenFromKeyring(keyringType, tokenName)
|
||||
if at == nil {
|
||||
return nil, errors.New(ctx, errors.Conflict, op, "no auth token available to send to daemon")
|
||||
}
|
||||
pa.Keyring = &daemon.KeyringToken{
|
||||
KeyringType: keyringType,
|
||||
TokenName: tokenName,
|
||||
}
|
||||
pa.AuthTokenId = at.Id
|
||||
}
|
||||
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return addToken(ctx, dotPath, &pa)
|
||||
}
|
||||
|
||||
func addToken(ctx context.Context, daemonPath string, p *daemon.UpsertTokenRequest) (*api.Error, error) {
|
||||
const op = "daemon.addToken"
|
||||
client, err := api.NewClient(nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
addr := daemon.SocketAddress(daemonPath)
|
||||
_, err = os.Stat(strings.TrimPrefix(addr, "unix://"))
|
||||
if strings.HasPrefix(addr, "unix://") && err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if err := client.SetAddr(addr); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// Because this is using the real lib it can pick up from stored locations
|
||||
// like the system keychain. Explicitly clear the token for now
|
||||
client.SetToken("")
|
||||
|
||||
req, err := client.NewRequest(ctx, "POST", "/tokens", p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add(daemon.VersionHeaderKey, version.Get().VersionNumber())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Decode(&struct{}{})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// CommandWrapper starts the boundary daemon after the command was Run and attempts
|
||||
// to send the current persona to any running daemon.
|
||||
type CommandWrapper struct {
|
||||
cli.Command
|
||||
ui cli.Ui
|
||||
}
|
||||
|
||||
// Wrap returns a cli.CommandFactory that returns a command wrapped in the CommandWrapper.
|
||||
func Wrap(ui cli.Ui, wrapped cli.Command) cli.CommandFactory {
|
||||
return func() (cli.Command, error) {
|
||||
return &CommandWrapper{
|
||||
Command: wrapped,
|
||||
ui: ui,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the wrapped command and then attempts to start the boundary daemon and send
|
||||
// the current persona
|
||||
func (w *CommandWrapper) Run(args []string) int {
|
||||
r := w.Command.Run(args)
|
||||
|
||||
ctx := context.Background()
|
||||
if w.startDaemon(ctx) {
|
||||
w.addPersonaInCache(ctx)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// startDaemon attempts to start a daemon and returns true if we have attempted to start
|
||||
// the daemon and either it was successful or it was already running.
|
||||
func (w *CommandWrapper) startDaemon(ctx context.Context) bool {
|
||||
cmdName, err := os.Executable()
|
||||
if err != nil {
|
||||
w.ui.Error(fmt.Sprintf("unable to find boundary binary for daemon startup: %s", err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
var stdErr bytes.Buffer
|
||||
cmd := exec.Command(cmdName, "daemon", "start", "-background")
|
||||
cmd.Stderr = &stdErr
|
||||
|
||||
// We use Run here instead of Start because the command spawns off a subprocess and returns.
|
||||
// We do not want to send the request to add a persona to the cache until we know the daemon
|
||||
// has started up.
|
||||
err = cmd.Run()
|
||||
return err == nil || strings.Contains(stdErr.String(), "already running")
|
||||
}
|
||||
|
||||
// addPersonaInCache runs AddPersonaCommand
|
||||
func (w *CommandWrapper) addPersonaInCache(ctx context.Context) bool {
|
||||
c := AddTokenCommand{Command: base.NewCommand(w.ui)}
|
||||
c.Flags()
|
||||
apiErr, err := c.Add(ctx)
|
||||
return err == nil && apiErr == nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/sevlyar/go-daemon"
|
||||
)
|
||||
|
||||
func writePidFile(ctx context.Context, pidFile string) (pidCleanup, error) {
|
||||
const op = "daemon.writePidFile"
|
||||
|
||||
// Determine if we should clean up the file after we are done or if
|
||||
// it should stick around in the case of lock aquision error since this
|
||||
// file didn't create it.
|
||||
var pidExists bool
|
||||
_, err := os.Stat(pidFile)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return noopPidCleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if err == nil {
|
||||
pidExists = true
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(pidFile, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return noopPidCleanup, errors.Wrap(ctx, err, op, errors.WithMsg("opening file"))
|
||||
}
|
||||
closeAndDeleteFileFn := func() error {
|
||||
err := f.Close()
|
||||
if !pidExists {
|
||||
err = stderrors.Join(err, os.Remove(pidFile))
|
||||
}
|
||||
return err
|
||||
}
|
||||
l := daemon.NewLockFile(f)
|
||||
if err := l.Lock(); err != nil {
|
||||
return closeAndDeleteFileFn, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// Now that we have aquired the lock and verified we own the pid file
|
||||
// we can remove it always when cleaning up.
|
||||
unlockAndCleanFn := func() error {
|
||||
return l.Remove()
|
||||
}
|
||||
if err := l.WritePid(); err != nil {
|
||||
return unlockAndCleanFn, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return unlockAndCleanFn, nil
|
||||
}
|
||||
|
||||
func pidFileInUse(ctx context.Context, pidFile string) (*os.Process, error) {
|
||||
const op = "daemon.pidFileInUse"
|
||||
if pidFile == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "pid filename is empty")
|
||||
}
|
||||
proc, err := (&daemon.Context{PidFileName: pidFile}).Search()
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
return proc, nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPidInUse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dotPath := t.TempDir()
|
||||
pidPath := filepath.Join(dotPath, "boundary.pid")
|
||||
|
||||
used, err := pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, used)
|
||||
|
||||
workingPidCleanup1, err := writePidFile(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, used)
|
||||
|
||||
failingPidCleanup, err := writePidFile(ctx, pidPath)
|
||||
assert.Error(t, err)
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, used)
|
||||
|
||||
assert.NoError(t, failingPidCleanup())
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, used)
|
||||
|
||||
assert.NoError(t, workingPidCleanup1())
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, used)
|
||||
|
||||
workingPidCleanup2, err := writePidFile(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, used)
|
||||
|
||||
assert.NoError(t, workingPidCleanup2())
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, used)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func writePidFile(ctx context.Context, pidFile string) (pidCleanup, error) {
|
||||
const op = "daemon.writePidFile"
|
||||
// Create the file for writing, and set shared read so following processes
|
||||
// cannot open this file for writing.
|
||||
fd, err := windows.CreateFile(&(windows.StringToUTF16(pidFile)[0]), windows.GENERIC_WRITE,
|
||||
windows.FILE_SHARE_READ, nil, windows.OPEN_ALWAYS, windows.FILE_ATTRIBUTE_NORMAL, 0)
|
||||
if err != nil {
|
||||
return noopPidCleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
cleanup := func() error {
|
||||
var err error
|
||||
if inErr := windows.CloseHandle(fd); inErr != nil {
|
||||
err = stderrors.Join(err, errors.Wrap(ctx, inErr, op, errors.WithMsg("handler close")))
|
||||
}
|
||||
if inErr := windows.DeleteFile(&(windows.StringToUTF16(pidFile)[0])); inErr != nil {
|
||||
err = stderrors.Join(err, errors.Wrap(ctx, inErr, op, errors.WithMsg("removing file")))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := windows.Seek(fd, 0, windows.FILE_BEGIN); err != nil {
|
||||
return cleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
b := bytes.NewBuffer(nil)
|
||||
if _, err := fmt.Fprint(b, os.Getpid()); err != nil {
|
||||
return cleanup, errors.Wrap(ctx, err, op, errors.WithMsg("writing file buffer"))
|
||||
}
|
||||
var fileLen int
|
||||
if fileLen, err = windows.Write(fd, b.Bytes()); err != nil {
|
||||
return cleanup, errors.Wrap(ctx, err, op, errors.WithMsg("writing buffer to file"))
|
||||
}
|
||||
if err = windows.Ftruncate(fd, int64(fileLen)); err != nil {
|
||||
return cleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return cleanup, windows.Fsync(fd)
|
||||
}
|
||||
|
||||
func pidFileInUse(ctx context.Context, pidFile string) (*os.Process, error) {
|
||||
const op = "daemon.pidFileInUse"
|
||||
if pidFile == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "pid filename is empty")
|
||||
}
|
||||
|
||||
var err error
|
||||
var file *os.File
|
||||
if file, err = os.OpenFile(pidFile, os.O_RDONLY, 0o640); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("OpenFile"))
|
||||
}
|
||||
if file == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer func() {
|
||||
file.Close()
|
||||
}()
|
||||
|
||||
if _, err = file.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pid int
|
||||
_, err = fmt.Fscan(file, &pid)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Fscan"))
|
||||
}
|
||||
|
||||
p, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "The parameter is incorrect") {
|
||||
return nil, errors.New(ctx, errors.NotFound, op, "cannot find process")
|
||||
}
|
||||
// we failed to get the process for whatever reason
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("FindProcess %d", pid))
|
||||
}
|
||||
if p == nil {
|
||||
return nil, errors.New(ctx, errors.NotFound, op, "cannot find process")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
const (
|
||||
dotDirname = ".boundary"
|
||||
pidFileName = "cache.pid"
|
||||
logFileName = "cache.log"
|
||||
|
||||
// Mark of process as having been started in the background
|
||||
backgroundEnvName = "_BOUNDARY_DAEMON_BACKGROUND"
|
||||
backgroundEnvVal = "1"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*StartCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*StartCommand)(nil)
|
||||
)
|
||||
|
||||
type server interface {
|
||||
setupLogging(context.Context, io.Writer) error
|
||||
serve(context.Context, daemon.Commander, net.Listener) error
|
||||
shutdown() error
|
||||
}
|
||||
|
||||
type StartCommand struct {
|
||||
*base.Command
|
||||
|
||||
flagRefreshIntervalSeconds int64
|
||||
flagDatabaseUrl string
|
||||
flagLogLevel string
|
||||
flagLogFormat string
|
||||
flagStoreDebug bool
|
||||
flagBackground bool
|
||||
}
|
||||
|
||||
func (c *StartCommand) Synopsis() string {
|
||||
return "Start a Boundary daemon"
|
||||
}
|
||||
|
||||
func (c *StartCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary daemon start [options]
|
||||
|
||||
Start a daemon:
|
||||
|
||||
$ boundary daemon start
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *StartCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient)
|
||||
|
||||
f := set.NewFlagSet("Command Options")
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "log-level",
|
||||
Target: &c.flagLogLevel,
|
||||
EnvVar: "BOUNDARY_LOG_LEVEL",
|
||||
Completion: complete.PredictSet("trace", "debug", "info", "warn", "err"),
|
||||
Usage: "Log verbosity level, mostly as a fallback for events. Supported values (in order of more detail to less) are " +
|
||||
"\"trace\", \"debug\", \"info\", \"warn\", and \"err\".",
|
||||
})
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "log-format",
|
||||
Target: &c.flagLogFormat,
|
||||
Completion: complete.PredictSet("standard", "json"),
|
||||
Usage: `Log format, mostly as a fallback for events. Supported values are "standard" and "json".`,
|
||||
})
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "database-url",
|
||||
Target: &c.flagDatabaseUrl,
|
||||
Usage: `If set, specifies the URL used to connect to the sqlite database (store) for caching. This can refer to a file on disk (file://) from which a URL will be read; an env var (env://) from which the URL will be read; or a direct database URL.`,
|
||||
})
|
||||
f.Int64Var(&base.Int64Var{
|
||||
Name: "refresh-interval-seconds",
|
||||
Target: &c.flagRefreshIntervalSeconds,
|
||||
Usage: `If set, specifies the number of seconds between cache refreshes. Default: 5 minutes`,
|
||||
Aliases: []string{"r"},
|
||||
Default: daemon.DefaultRefreshIntervalSeconds,
|
||||
})
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "store-debug",
|
||||
Target: &c.flagStoreDebug,
|
||||
Default: false,
|
||||
Usage: `Turn on store debugging`,
|
||||
Aliases: []string{"d"},
|
||||
})
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "background",
|
||||
Target: &c.flagBackground,
|
||||
Default: false,
|
||||
Usage: `Turn on store debugging`,
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *StartCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *StartCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *StartCommand) Run(args []string) int {
|
||||
const op = "daemon.(StartCommand).Run"
|
||||
ctx, cancel := context.WithCancel(c.Context)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
dotDir, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
continueRun, writers, cleanup, err := makeBackground(ctx, dotDir, c.flagBackground)
|
||||
defer func() {
|
||||
if cleanup != nil {
|
||||
cleanup()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
if !continueRun {
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
// TODO: print something out for the spawner to consume in case they can easily
|
||||
// report if the daemon started or not.
|
||||
|
||||
logFilePath := filepath.Join(dotDir, logFileName)
|
||||
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
defer logFile.Close()
|
||||
if _, err := logFile.Seek(0, io.SeekEnd); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
writers = append(writers, logFile)
|
||||
|
||||
cfg := &daemon.Config{
|
||||
ContextCancel: cancel,
|
||||
RefreshIntervalSeconds: c.flagRefreshIntervalSeconds,
|
||||
DatabaseUrl: c.flagDatabaseUrl,
|
||||
StoreDebug: c.flagStoreDebug,
|
||||
LogLevel: c.flagLogLevel,
|
||||
LogFormat: c.flagLogFormat,
|
||||
LogWriter: io.MultiWriter(writers...),
|
||||
DotDirectory: dotDir,
|
||||
}
|
||||
|
||||
srv, err := daemon.New(ctx, cfg)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var srvErr error
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
srvErr = srv.Serve(ctx, c)
|
||||
}()
|
||||
|
||||
// This is a blocking call. We rely on the c.ShutdownCh to cancel this
|
||||
// context when sigterm or sigint is received.
|
||||
<-ctx.Done()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
wg.Wait()
|
||||
if srvErr != nil {
|
||||
c.PrintCliError(srvErr)
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
// DefaultDotDirectory returns the default path to the boundary dot directory.
|
||||
func DefaultDotDirectory(ctx context.Context) (string, error) {
|
||||
const op = "daemon.DefaultDotDirectory"
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return filepath.Join(homeDir, dotDirname), nil
|
||||
}
|
||||
|
||||
func makeBackground(ctx context.Context, dotDir string, runBackgroundFlag bool) (bool, []io.Writer, pidCleanup, error) {
|
||||
const op = "daemon.makeBackground"
|
||||
|
||||
writers := []io.Writer{}
|
||||
pidPath := filepath.Join(dotDir, pidFileName)
|
||||
if running, err := pidFileInUse(ctx, pidPath); running != nil {
|
||||
return false, writers, noopPidCleanup, errors.New(ctx, errors.Conflict, op, "daemon already running")
|
||||
} else if err != nil && !errors.Match(errors.T(errors.NotFound), err) {
|
||||
return false, writers, noopPidCleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
if !runBackgroundFlag && os.Getenv(backgroundEnvName) != backgroundEnvVal {
|
||||
writers = append(writers, os.Stderr)
|
||||
}
|
||||
|
||||
if !runBackgroundFlag || os.Getenv(backgroundEnvName) == backgroundEnvVal {
|
||||
// We are either already running in the background or background was
|
||||
// not requested. Write the pid file and continue.
|
||||
cleanup, err := writePidFile(ctx, pidPath)
|
||||
if err != nil {
|
||||
return false, writers, noopPidCleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return true, writers, cleanup, nil
|
||||
}
|
||||
|
||||
absPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return false, writers, noopPidCleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
env := os.Environ()
|
||||
env = append(env, fmt.Sprintf("%s=%s", backgroundEnvName, backgroundEnvVal))
|
||||
cmd := exec.Command(absPath, "daemon", "start")
|
||||
cmd.Env = env
|
||||
if err = cmd.Start(); err != nil {
|
||||
return false, writers, noopPidCleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
// TODO: Read the output from the child process for a brief time
|
||||
// to see if we can identify any errors that might arise.
|
||||
return false, writers, noopPidCleanup, nil
|
||||
}
|
||||
|
||||
type pidCleanup func() error
|
||||
|
||||
var noopPidCleanup pidCleanup = func() error { return nil }
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderr "errors"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type StopCommand struct {
|
||||
*base.Command
|
||||
}
|
||||
|
||||
func (c *StopCommand) Synopsis() string {
|
||||
return "Stop a Boundary daemon"
|
||||
}
|
||||
|
||||
func (c *StopCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary daemon stop
|
||||
|
||||
Stop a daemon:
|
||||
|
||||
$ boundary daemon stop
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *StopCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient)
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *StopCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *StopCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *StopCommand) Run(args []string) int {
|
||||
ctx, cancel := context.WithCancel(c.Context)
|
||||
c.Context = ctx
|
||||
c.ContextCancel = cancel
|
||||
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
if err := c.stop(c.Context); err != nil {
|
||||
if errors.Match(errors.T(errors.NotFound), err) {
|
||||
c.PrintCliError(stderr.New("deamon process is not running"))
|
||||
} else {
|
||||
c.PrintCliError(err)
|
||||
}
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
return base.CommandSuccess
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// stop will send a term signal to the daemon to shut down.
|
||||
func (s *StopCommand) stop(ctx context.Context) error {
|
||||
const op = "daemon.(StopCommand).stop"
|
||||
switch {
|
||||
case util.IsNil(ctx):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "context is missing")
|
||||
}
|
||||
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
pidPath := filepath.Join(dotPath, pidFileName)
|
||||
p, err := pidFileInUse(ctx, pidPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if p == nil {
|
||||
return errors.New(ctx, errors.NotFound, op, "daemon not running")
|
||||
}
|
||||
if err := p.Signal(syscall.SIGTERM); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("sending sigterm to process"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/boundary/version"
|
||||
)
|
||||
|
||||
// stop will send a term signal to the daemon to shut down.
|
||||
func (s *StopCommand) stop(ctx context.Context) error {
|
||||
const op = "daemon.(StopCommand).stop"
|
||||
switch {
|
||||
case util.IsNil(ctx):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "context is missing")
|
||||
}
|
||||
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
apiErr, err := stopThroughHandler(ctx, dotPath)
|
||||
switch {
|
||||
case err != nil, apiErr != nil:
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
} else if apiErr != nil {
|
||||
errMsg = apiErr.Message
|
||||
}
|
||||
s.UI.Warn(fmt.Sprintf("failed stopping the daemon through the handler: %q, now killing the process", errMsg))
|
||||
default:
|
||||
// there wasn't an error stopping it through the handler. No need to
|
||||
// force kill the process
|
||||
return nil
|
||||
}
|
||||
|
||||
pidPath := filepath.Join(dotPath, pidFileName)
|
||||
p, err := pidFileInUse(ctx, pidPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.NotFound))
|
||||
}
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("Unable to stop the daemon"))
|
||||
}
|
||||
if p == nil {
|
||||
return errors.New(ctx, errors.NotFound, op, "daemon process was not found")
|
||||
}
|
||||
|
||||
if err := p.Kill(); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("killing the daemon"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopThroughHandler(ctx context.Context, dotPath string) (*api.Error, error) {
|
||||
const op = "daemon.stopThroughHandler"
|
||||
|
||||
sockAddr := daemon.SocketAddress(dotPath)
|
||||
client, err := api.NewClient(nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if err := client.SetAddr(sockAddr); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// Because this is using the real lib it can pick up from stored locations
|
||||
// like the system keychain. Explicitly clear the token for now
|
||||
client.SetToken("")
|
||||
|
||||
req, err := client.NewRequest(ctx, "POST", "/stop", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add(daemon.VersionHeaderKey, version.Get().VersionNumber())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Decode(&struct{}{})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
/*
|
||||
Package cmd contains the command packages to the client cache. Adding
|
||||
these commands to the boundary cli is the only integration between the boundary
|
||||
binary and the client cache.
|
||||
*/
|
||||
package cmd
|
||||
@@ -0,0 +1,335 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/sessions"
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
daemoncmd "github.com/hashicorp/boundary/internal/clientcache/cmd/daemon"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/version"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*SearchCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*SearchCommand)(nil)
|
||||
|
||||
supportedResourceTypes = []string{
|
||||
"targets",
|
||||
"sessions",
|
||||
}
|
||||
)
|
||||
|
||||
type SearchCommand struct {
|
||||
*base.Command
|
||||
flagNameStartsWith string
|
||||
flagQuery string
|
||||
flagResource string
|
||||
}
|
||||
|
||||
func (c *SearchCommand) Synopsis() string {
|
||||
return "Search resources in boundary"
|
||||
}
|
||||
|
||||
func (c *SearchCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary search [options]
|
||||
|
||||
Search a boundary resource:
|
||||
|
||||
$ boundary search -resource targets
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *SearchCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetClient | base.FlagSetOutputFormat)
|
||||
|
||||
f := set.NewFlagSet("Command Options")
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "name-starts-with",
|
||||
Target: &c.flagNameStartsWith,
|
||||
Usage: `If set, specifies to search for a target that starts with`,
|
||||
})
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "query",
|
||||
Target: &c.flagQuery,
|
||||
Usage: `If set, specifies the target search query`,
|
||||
})
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "resource",
|
||||
Target: &c.flagResource,
|
||||
Usage: `Specifies the resource type to search over`,
|
||||
Completion: complete.PredictSet(supportedResourceTypes...),
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *SearchCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *SearchCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *SearchCommand) Run(args []string) int {
|
||||
ctx := c.Context
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
switch {
|
||||
case slices.Contains(supportedResourceTypes, c.flagResource):
|
||||
case c.flagResource == "":
|
||||
c.PrintCliError(stderrors.New("Resource is required but not passed in via -resource"))
|
||||
return base.CommandUserError
|
||||
default:
|
||||
c.PrintCliError(stderrors.New("The value passed in with -resource is not currently supported in search"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
resp, err := c.Search(ctx)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
res := &daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(res)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
if apiErr != nil {
|
||||
c.PrintApiError(apiErr, "Error from daemon when performing search")
|
||||
return base.CommandApiError
|
||||
}
|
||||
switch base.Format(c.UI) {
|
||||
case "json":
|
||||
c.UI.Output(resp.Body.String())
|
||||
default:
|
||||
switch {
|
||||
case len(res.Targets) > 0:
|
||||
c.UI.Output(printTargetListTable(res.Targets))
|
||||
case len(res.Sessions) > 0:
|
||||
c.UI.Output(printSessionListTable(res.Sessions))
|
||||
default:
|
||||
c.UI.Output("No items found")
|
||||
}
|
||||
}
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
func (c *SearchCommand) Search(ctx context.Context) (*api.Response, error) {
|
||||
keyringType, tokenName, err := c.DiscoverKeyringTokenInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tf := filterBy{
|
||||
flagQuery: c.flagQuery,
|
||||
resource: c.flagResource,
|
||||
}
|
||||
at := c.ReadTokenFromKeyring(keyringType, tokenName)
|
||||
if at != nil {
|
||||
tf.authTokenId = at.Id
|
||||
}
|
||||
|
||||
dotPath, err := daemoncmd.DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return search(ctx, dotPath, tf)
|
||||
}
|
||||
|
||||
func search(ctx context.Context, daemonPath string, fb filterBy) (*api.Response, error) {
|
||||
const op = "search.search"
|
||||
client, err := api.NewClient(nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
addr := daemon.SocketAddress(daemonPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if _, err := os.Stat(strings.TrimPrefix(addr, "unix://")); strings.HasPrefix(addr, "unix://") && err == os.ErrNotExist {
|
||||
return nil, errors.New(ctx, errors.Internal, op, "daemon unix socket is not setup")
|
||||
}
|
||||
if err := client.SetAddr(addr); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
req, err := client.NewRequest(ctx, "GET", "/search", nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("new client request error"))
|
||||
}
|
||||
req.Header.Add(daemon.VersionHeaderKey, version.Get().VersionNumber())
|
||||
q := url.Values{}
|
||||
q.Add("auth_token_id", fb.authTokenId)
|
||||
q.Add("resource", fb.resource)
|
||||
q.Add("query", fb.flagQuery)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("client do failed"))
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func printTargetListTable(items []*targets.Target) string {
|
||||
if len(items) == 0 {
|
||||
return "No targets found"
|
||||
}
|
||||
var output []string
|
||||
output = []string{
|
||||
"",
|
||||
"Target 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.Address != "" {
|
||||
output = append(output,
|
||||
fmt.Sprintf(" Address: %s", item.Address),
|
||||
)
|
||||
}
|
||||
if len(item.AuthorizedActions) > 0 {
|
||||
output = append(output,
|
||||
" Authorized Actions:",
|
||||
base.WrapSlice(6, item.AuthorizedActions),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return base.WrapForHelpText(output)
|
||||
}
|
||||
|
||||
func printSessionListTable(items []*sessions.Session) string {
|
||||
if len(items) == 0 {
|
||||
return "No sessions found"
|
||||
}
|
||||
var output []string
|
||||
output = []string{
|
||||
"",
|
||||
"Session 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.Status != "" {
|
||||
output = append(output,
|
||||
fmt.Sprintf(" Status: %s", item.Status),
|
||||
)
|
||||
}
|
||||
if !item.CreatedTime.IsZero() {
|
||||
output = append(output,
|
||||
fmt.Sprintf(" Created Time: %s", item.CreatedTime.Local().Format(time.RFC1123)),
|
||||
)
|
||||
}
|
||||
if !item.ExpirationTime.IsZero() {
|
||||
output = append(output,
|
||||
fmt.Sprintf(" Expiration Time: %s", item.ExpirationTime.Local().Format(time.RFC1123)),
|
||||
)
|
||||
}
|
||||
if !item.UpdatedTime.IsZero() {
|
||||
output = append(output,
|
||||
fmt.Sprintf(" Updated Time: %s", item.UpdatedTime.Local().Format(time.RFC1123)),
|
||||
)
|
||||
}
|
||||
if item.UserId != "" {
|
||||
output = append(output,
|
||||
fmt.Sprintf(" User ID: %s", item.UserId),
|
||||
)
|
||||
}
|
||||
if item.TargetId != "" {
|
||||
output = append(output,
|
||||
fmt.Sprintf(" Target ID: %s", item.TargetId),
|
||||
)
|
||||
}
|
||||
if len(item.AuthorizedActions) > 0 {
|
||||
output = append(output,
|
||||
" Authorized Actions:",
|
||||
base.WrapSlice(6, item.AuthorizedActions),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return base.WrapForHelpText(output)
|
||||
}
|
||||
|
||||
type filterBy struct {
|
||||
flagQuery string
|
||||
authTokenId string
|
||||
resource string
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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/daemon"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testCommander struct {
|
||||
t *testing.T
|
||||
at *authtokens.AuthToken
|
||||
}
|
||||
|
||||
func (r *testCommander) keyring() string {
|
||||
return r.at.Id
|
||||
}
|
||||
|
||||
func (r *testCommander) tokenName() string {
|
||||
return r.at.Id
|
||||
}
|
||||
|
||||
func (r *testCommander) Client(opt ...base.Option) (*api.Client, error) {
|
||||
client, err := api.NewClient(nil)
|
||||
require.NoError(r.t, err)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *testCommander) DiscoverKeyringTokenInfo() (string, string, error) {
|
||||
return r.keyring(), r.tokenName(), nil
|
||||
}
|
||||
|
||||
func (r *testCommander) ReadTokenFromKeyring(k, a string) *authtokens.AuthToken {
|
||||
return r.at
|
||||
}
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
UserId: "user_1",
|
||||
Token: "at_1_token",
|
||||
ExpirationTime: time.Now().Add(time.Minute),
|
||||
}
|
||||
cmd := &testCommander{t: t, at: at}
|
||||
boundaryTokenReaderFn := func(ctx context.Context, addr, authToken string) (*authtokens.AuthToken, error) {
|
||||
if authToken == at.Token {
|
||||
return at, nil
|
||||
}
|
||||
return nil, errors.New("test not found error")
|
||||
}
|
||||
|
||||
srv := daemon.NewTestServer(t, cmd)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
srv.Serve(t, daemon.WithBoundaryTokenReaderFunc(ctx, boundaryTokenReaderFn))
|
||||
}()
|
||||
// Give the store some time to get initialized
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
errorCases := []struct {
|
||||
name string
|
||||
fb filterBy
|
||||
apiErrContains string
|
||||
}{
|
||||
{
|
||||
name: "no resource",
|
||||
fb: filterBy{
|
||||
flagQuery: "name=name",
|
||||
authTokenId: at.Id,
|
||||
},
|
||||
apiErrContains: "resource is a required field but was empty",
|
||||
},
|
||||
{
|
||||
name: "bad resource",
|
||||
fb: filterBy{
|
||||
authTokenId: at.Id,
|
||||
flagQuery: "name=name",
|
||||
resource: "hosts",
|
||||
},
|
||||
apiErrContains: "provided resource is not a valid searchable resource",
|
||||
},
|
||||
{
|
||||
name: "unknown auth token id",
|
||||
fb: filterBy{
|
||||
authTokenId: "unknown",
|
||||
flagQuery: "description % tar",
|
||||
resource: "targets",
|
||||
},
|
||||
apiErrContains: "Forbidden",
|
||||
},
|
||||
{
|
||||
name: "unsupported column",
|
||||
fb: filterBy{
|
||||
authTokenId: at.Id,
|
||||
flagQuery: "item % 'tar'",
|
||||
resource: "targets",
|
||||
},
|
||||
apiErrContains: "invalid column \"item\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range errorCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), tc.fb)
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, tc.apiErrContains)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("empty response from list", func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
|
||||
authTokenId: at.Id,
|
||||
resource: "targets",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.NotNil(t, r)
|
||||
assert.EqualValues(t, r, daemon.SearchResult{})
|
||||
})
|
||||
|
||||
t.Run("empty response from query", func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
|
||||
authTokenId: at.Id,
|
||||
flagQuery: "name=name",
|
||||
resource: "targets",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.NotNil(t, r)
|
||||
assert.EqualValues(t, r, daemon.SearchResult{})
|
||||
})
|
||||
|
||||
srv.AddResources(t, cmd.at, []*targets.Target{
|
||||
{Id: "ttcp_1234567890", Name: "name1", Description: "description1"},
|
||||
{Id: "ttcp_0987654321", Name: "name2", Description: "description2"},
|
||||
}, []*sessions.Session{
|
||||
{Id: "sess_1234567890", TargetId: "ttcp_1234567890", Status: "pending"},
|
||||
{Id: "sess_0987654321", TargetId: "ttcp_0987654321", Status: "pending"},
|
||||
}, boundaryTokenReaderFn)
|
||||
|
||||
t.Run("target response from list", func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
|
||||
authTokenId: at.Id,
|
||||
resource: "targets",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.NotNil(t, r)
|
||||
assert.Len(t, r.Targets, 2)
|
||||
})
|
||||
t.Run("full target response from query", func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
|
||||
authTokenId: at.Id,
|
||||
flagQuery: "id % ttcp",
|
||||
resource: "targets",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.NotNil(t, r)
|
||||
assert.Len(t, r.Targets, 2)
|
||||
})
|
||||
t.Run("partial target response from query", func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
|
||||
authTokenId: at.Id,
|
||||
flagQuery: "id % ttcp_1234567890",
|
||||
resource: "targets",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.NotNil(t, r)
|
||||
assert.Len(t, r.Sessions, 0)
|
||||
assert.Len(t, r.Targets, 1)
|
||||
})
|
||||
|
||||
t.Run("session response from list", func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
|
||||
authTokenId: at.Id,
|
||||
resource: "sessions",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.NotNil(t, r)
|
||||
assert.Len(t, r.Targets, 0)
|
||||
assert.Len(t, r.Sessions, 2)
|
||||
})
|
||||
t.Run("full session response from query", func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
|
||||
authTokenId: at.Id,
|
||||
flagQuery: "id % sess",
|
||||
resource: "sessions",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.NotNil(t, r)
|
||||
assert.Len(t, r.Sessions, 2)
|
||||
})
|
||||
t.Run("partial session response from query", func(t *testing.T) {
|
||||
resp, err := search(ctx, srv.BaseDotDir(), filterBy{
|
||||
authTokenId: at.Id,
|
||||
flagQuery: "id % sess_1234567890",
|
||||
resource: "sessions",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
r := daemon.SearchResult{}
|
||||
apiErr, err := resp.Decode(&r)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.NotNil(t, r)
|
||||
assert.Len(t, r.Sessions, 1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
/*
|
||||
Package clientcache contains the boundary client cache.
|
||||
The client cache is a cache that runs on a client's machine and caches a subset
|
||||
of boundary resources and makes them available for functionality like searching.
|
||||
*/
|
||||
package clientcache
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
/*
|
||||
Package cache contains the domain logic for the client cache.
|
||||
*/
|
||||
package cache
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
withUpdateLastAccessedTime bool
|
||||
withDbType dbw.DbType
|
||||
withTargetRetrievalFunc TargetRetrievalFunc
|
||||
withSessionRetrievalFunc SessionRetrievalFunc
|
||||
}
|
||||
|
||||
// Option - how options are passed as args
|
||||
type Option func(*options) error
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{
|
||||
withDbType: dbw.Sqlite,
|
||||
}
|
||||
}
|
||||
|
||||
func getOpts(opt ...Option) (options, error) {
|
||||
opts := getDefaultOptions()
|
||||
|
||||
for _, o := range opt {
|
||||
if err := o(&opts); err != nil {
|
||||
return opts, err
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// WithUpdateLastAccessedTime provides an option for updating the last access time
|
||||
func WithUpdateLastAccessedTime(b bool) Option {
|
||||
return func(o *options) error {
|
||||
o.withUpdateLastAccessedTime = b
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTargetRetrievalFunc provides an option for specifying a targetRetrievalFunc
|
||||
func WithTargetRetrievalFunc(fn TargetRetrievalFunc) Option {
|
||||
return func(o *options) error {
|
||||
o.withTargetRetrievalFunc = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSessionRetrievalFunc provides an option for specifying a sessionRetrievalFunc
|
||||
func WithSessionRetrievalFunc(fn SessionRetrievalFunc) Option {
|
||||
return func(o *options) error {
|
||||
o.withSessionRetrievalFunc = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api/sessions"
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
opts, err := getOpts()
|
||||
require.NoError(t, err)
|
||||
testOpts := options{
|
||||
withDbType: dbw.Sqlite,
|
||||
}
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithUpdateLastAccessedTime", func(t *testing.T) {
|
||||
opts, err := getOpts(WithUpdateLastAccessedTime(true))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withUpdateLastAccessedTime = true
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithTargetRetrievalFunc", func(t *testing.T) {
|
||||
var f TargetRetrievalFunc = func(ctx context.Context, keyringstring, tokenName string) ([]*targets.Target, error) { return nil, nil }
|
||||
opts, err := getOpts(WithTargetRetrievalFunc(f))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, opts.withTargetRetrievalFunc)
|
||||
opts.withTargetRetrievalFunc = nil
|
||||
|
||||
testOpts := getDefaultOptions()
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithSessionRetrievalFunc", func(t *testing.T) {
|
||||
var f SessionRetrievalFunc = func(ctx context.Context, keyringstring, tokenName string) ([]*sessions.Session, error) {
|
||||
return nil, nil
|
||||
}
|
||||
opts, err := getOpts(WithSessionRetrievalFunc(f))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, opts.withSessionRetrievalFunc)
|
||||
opts.withSessionRetrievalFunc = nil
|
||||
|
||||
testOpts := getDefaultOptions()
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
type RefreshService struct {
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
func NewRefreshService(ctx context.Context, r *Repository) (*RefreshService, error) {
|
||||
return &RefreshService{repo: r}, nil
|
||||
}
|
||||
|
||||
// cleanAndPickAuthTokens removes from the cache all auth tokens which are
|
||||
// evicted from the cache or no longer stored in a keyring and returns the
|
||||
// remaining ones.
|
||||
// The returned auth tokens have not been validated against boundary
|
||||
func (r *RefreshService) cleanAndPickAuthTokens(ctx context.Context, u *user) (map[AuthToken]string, error) {
|
||||
const op = "cache.(RefreshService).cleanAndPickAuthTokens"
|
||||
switch {
|
||||
case util.IsNil(u):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
}
|
||||
ret := make(map[AuthToken]string)
|
||||
|
||||
tokens, err := r.repo.listTokens(ctx, u)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
for _, t := range tokens {
|
||||
keyringTokens, err := r.repo.listKeyringTokens(ctx, t)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("for user %v, auth token %q", u, t.Id))
|
||||
}
|
||||
for _, kt := range keyringTokens {
|
||||
at := r.repo.tokenKeyringFn(kt.KeyringType, kt.TokenName)
|
||||
switch {
|
||||
case at == nil, at.Id != kt.AuthTokenId, at.UserId != t.UserId:
|
||||
// delete the keyring token if the auth token in the keyring
|
||||
// has changed since it was stored in the cache.
|
||||
if err := r.repo.deleteKeyringToken(ctx, *kt); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
case at != nil:
|
||||
_, err := r.repo.tokenReadFromBoundaryFn(ctx, u.Address, at.Token)
|
||||
var apiErr *api.Error
|
||||
switch {
|
||||
case err != nil && api.ErrUnauthorized.Is(err):
|
||||
if err := r.repo.deleteKeyringToken(ctx, *kt); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
continue
|
||||
case err != nil && !errors.Is(err, apiErr):
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("validating keyring stored token against boundary", "auth token id", at.Id))
|
||||
continue
|
||||
}
|
||||
ret[*t] = at.Token
|
||||
}
|
||||
}
|
||||
if atv, ok := r.repo.idToKeyringlessAuthToken.Load(t.Id); ok {
|
||||
if at, ok := atv.(*authtokens.AuthToken); ok {
|
||||
_, err := r.repo.tokenReadFromBoundaryFn(ctx, u.Address, at.Token)
|
||||
var apiErr *api.Error
|
||||
switch {
|
||||
case err != nil && api.ErrUnauthorized.Is(err):
|
||||
r.repo.idToKeyringlessAuthToken.Delete(t.Id)
|
||||
continue
|
||||
case err != nil && !errors.Is(err, apiErr):
|
||||
event.WriteError(ctx, op, err, event.WithInfoMsg("validating in memory stored token against boundary", "auth token id", at.Id))
|
||||
continue
|
||||
}
|
||||
|
||||
ret[*t] = at.Token
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Refresh iterates over all tokens in the cache, attempts to read the
|
||||
// resources for those tokens from boundary and updates the cache with
|
||||
// the values retrieved there. Refresh accepts the options
|
||||
// WithTarget which overwrites the default function used to retrieve the
|
||||
// targets from a boundary address.
|
||||
func (r *RefreshService) Refresh(ctx context.Context, opt ...Option) error {
|
||||
const op = "cache.(RefreshService).Refresh"
|
||||
if err := r.repo.cleanExpiredOrOrphanedAuthTokens(ctx); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if err := r.repo.syncKeyringlessTokensWithDb(ctx); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
us, err := r.repo.listUsers(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
var retErr error
|
||||
for _, u := range us {
|
||||
tokens, err := r.cleanAndPickAuthTokens(ctx, u)
|
||||
if err != nil {
|
||||
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := r.repo.refreshTargets(ctx, u, tokens, opt...); err != nil {
|
||||
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op))
|
||||
}
|
||||
if err := r.repo.refreshSessions(ctx, u, tokens, opt...); err != nil {
|
||||
retErr = stderrors.Join(retErr, errors.Wrap(ctx, err, op))
|
||||
}
|
||||
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
// 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",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
usersLimit = 50
|
||||
tokenStalenessLimit = 36 * time.Hour
|
||||
)
|
||||
|
||||
// KeyringTokenLookupFn takes a token name and returns the token from the keyring
|
||||
type KeyringTokenLookupFn func(keyring string, tokenName string) *authtokens.AuthToken
|
||||
|
||||
// BoundaryTokenReaderFn reads an auth token's resource information from boundary
|
||||
type BoundaryTokenReaderFn func(ctx context.Context, addr string, authToken string) (*authtokens.AuthToken, error)
|
||||
|
||||
type Repository struct {
|
||||
rw *db.Db
|
||||
tokenKeyringFn KeyringTokenLookupFn
|
||||
tokenReadFromBoundaryFn BoundaryTokenReaderFn
|
||||
// idToKeyringlessAuthToken maps an auth token id to an *authtokens.AuthToken
|
||||
idToKeyringlessAuthToken *sync.Map
|
||||
}
|
||||
|
||||
// NewRepository returns a cache repository. The provided Store must be
|
||||
func NewRepository(ctx context.Context, conn *db.DB, idToAuthToken *sync.Map, keyringFn KeyringTokenLookupFn, atReadFn BoundaryTokenReaderFn, opt ...Option) (*Repository, error) {
|
||||
const op = "cache.NewRepository"
|
||||
switch {
|
||||
case util.IsNil(conn):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing store connection")
|
||||
case util.IsNil(idToAuthToken):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing keyringless auth token map")
|
||||
case util.IsNil(keyringFn):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token keyring function")
|
||||
case util.IsNil(atReadFn):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token read function")
|
||||
}
|
||||
return &Repository{
|
||||
rw: db.New(conn),
|
||||
tokenKeyringFn: keyringFn,
|
||||
tokenReadFromBoundaryFn: atReadFn,
|
||||
// This is passed in instead of being fully owned by the repo so multiple
|
||||
// instances of the repo can operate on the same backing data
|
||||
idToKeyringlessAuthToken: idToAuthToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) SaveError(ctx context.Context, u *user, resourceType string, err error) error {
|
||||
const op = "cache.(Repository).StoreError"
|
||||
switch {
|
||||
case resourceType == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "resource type is empty")
|
||||
case err == nil:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "error is nil")
|
||||
case u == nil:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
}
|
||||
apiErr := &ApiError{
|
||||
UserId: u.Id,
|
||||
ResourceType: resourceType,
|
||||
Error: err.Error(),
|
||||
}
|
||||
onConflict := db.OnConflict{
|
||||
Target: db.Columns{"user_id", "resource_type"},
|
||||
Action: db.SetColumns([]string{"error", "create_time"}),
|
||||
}
|
||||
if err := r.rw.Create(ctx, apiErr, db.WithOnConflict(&onConflict)); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ApiError struct {
|
||||
UserId string `gorm:"primaryKey"`
|
||||
ResourceType string `gorm:"primaryKey"`
|
||||
Error string
|
||||
CreateTime time.Time
|
||||
}
|
||||
|
||||
func (*ApiError) TableName() string {
|
||||
return "api_error"
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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/sessions"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/mql"
|
||||
)
|
||||
|
||||
// SessionRetrievalFunc is a function that retrieves sessions
|
||||
// from the provided boundary addr using the provided token.
|
||||
type SessionRetrievalFunc func(ctx context.Context, addr, token string) ([]*sessions.Session, error)
|
||||
|
||||
func defaultSessionFunc(ctx context.Context, addr, token string) ([]*sessions.Session, error) {
|
||||
const op = "cache.defaultSessionFunc"
|
||||
client, err := api.NewClient(&api.Config{
|
||||
Addr: addr,
|
||||
Token: token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
sClient := sessions.NewClient(client)
|
||||
l, err := sClient.List(ctx, "global", sessions.WithRecursive(true))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return l.Items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
|
||||
const op = "cache.(Repository).refreshSessions"
|
||||
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")
|
||||
}
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if opts.withSessionRetrievalFunc == nil {
|
||||
opts.withSessionRetrievalFunc = defaultSessionFunc
|
||||
}
|
||||
|
||||
// Find and use a token for retrieving sessions
|
||||
var gotResponse bool
|
||||
var resp []*sessions.Session
|
||||
var retErr error
|
||||
for at, t := range tokens {
|
||||
resp, err = opts.withSessionRetrievalFunc(ctx, u.Address, t)
|
||||
if err != nil {
|
||||
// TODO: If we get an error about the token no longer having
|
||||
// permissions, remove it.
|
||||
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(ctx, u, resource.Session.String(), retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
if !gotResponse {
|
||||
return retErr
|
||||
}
|
||||
|
||||
event.WriteSysEvent(ctx, op, fmt.Sprintf("updating %d sessions for user %v", len(resp), u))
|
||||
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
// TODO: Instead of deleting everything, use refresh tokens and apply the delta
|
||||
if _, err := w.Exec(ctx, "delete from session where user_id = @user_id",
|
||||
[]any{sql.Named("user_id", u.Id)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range resp {
|
||||
item, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newSession := &Session{
|
||||
UserId: u.Id,
|
||||
Id: s.Id,
|
||||
Type: s.Type,
|
||||
Status: s.Status,
|
||||
Endpoint: s.Endpoint,
|
||||
Item: string(item),
|
||||
}
|
||||
onConflict := db.OnConflict{
|
||||
Target: db.Columns{"user_id", "id"},
|
||||
Action: db.UpdateAll(true),
|
||||
}
|
||||
if err := w.Create(ctx, newSession, db.WithOnConflict(&onConflict)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListSessions(ctx context.Context, authTokenId string) ([]*sessions.Session, error) {
|
||||
const op = "cache.(Repository).ListSessions"
|
||||
switch {
|
||||
case authTokenId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is missing")
|
||||
}
|
||||
ret, err := r.searchSessions(ctx, authTokenId, "true", nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *Repository) QuerySessions(ctx context.Context, authTokenId, query string) ([]*sessions.Session, error) {
|
||||
const op = "cache.(Repository).QuerySessions"
|
||||
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, Session{}, mql.WithIgnoredFields("UserId", "Item"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
|
||||
}
|
||||
ret, err := r.searchSessions(ctx, authTokenId, w.Condition, w.Args)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *Repository) searchSessions(ctx context.Context, authTokenId, condition string, searchArgs []any) ([]*sessions.Session, error) {
|
||||
const op = "cache.(Repository).searchSessions"
|
||||
switch {
|
||||
case authTokenId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is missing")
|
||||
case condition == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "condition is missing")
|
||||
}
|
||||
|
||||
condition = fmt.Sprintf("%s and user_id in (select user_id from auth_token where id = ?)", condition)
|
||||
args := append(searchArgs, authTokenId)
|
||||
var cachedSessions []*Session
|
||||
if err := r.rw.SearchWhere(ctx, &cachedSessions, condition, args, db.WithLimit(-1)); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
retSessions := make([]*sessions.Session, 0, len(cachedSessions))
|
||||
for _, cachedSess := range cachedSessions {
|
||||
var sess sessions.Session
|
||||
if err := json.Unmarshal([]byte(cachedSess.Item), &sess); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
retSessions = append(retSessions, &sess)
|
||||
}
|
||||
return retSessions, nil
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
UserId string `gorm:"primaryKey"`
|
||||
Id string `gorm:"primaryKey"`
|
||||
Type string `gorm:"default:null"`
|
||||
Endpoint string `gorm:"default:null"`
|
||||
Status string `gorm:"default:null"`
|
||||
Item string `gorm:"default:null"`
|
||||
}
|
||||
|
||||
func (*Session) TableName() string {
|
||||
return "session"
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/api/sessions"
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/daemon/controller"
|
||||
"github.com/hashicorp/boundary/internal/daemon/worker"
|
||||
"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_refreshSessions(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 := []*sessions.Session{
|
||||
{
|
||||
Id: "ttcp_1",
|
||||
Status: "status1",
|
||||
Endpoint: "address1",
|
||||
Type: "tcp",
|
||||
},
|
||||
{
|
||||
Id: "ttcp_2",
|
||||
Status: "status2",
|
||||
Endpoint: "address2",
|
||||
Type: "tcp",
|
||||
},
|
||||
{
|
||||
Id: "ttcp_3",
|
||||
Status: "status3",
|
||||
Endpoint: "address3",
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
u *user
|
||||
sess []*sessions.Session
|
||||
wantCount int
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
u: &user{
|
||||
Address: addr,
|
||||
Id: at.UserId,
|
||||
},
|
||||
sess: ss,
|
||||
wantCount: len(ss),
|
||||
},
|
||||
{
|
||||
name: "repeated session with different values",
|
||||
u: &user{
|
||||
Address: addr,
|
||||
Id: at.UserId,
|
||||
},
|
||||
sess: append(ss, &sessions.Session{
|
||||
Id: ss[0].Id,
|
||||
Status: "a different status",
|
||||
}),
|
||||
wantCount: len(ss),
|
||||
},
|
||||
{
|
||||
name: "nil user",
|
||||
u: nil,
|
||||
sess: ss,
|
||||
errorContains: "user is nil",
|
||||
},
|
||||
{
|
||||
name: "missing user Id",
|
||||
u: &user{
|
||||
Address: addr,
|
||||
},
|
||||
sess: ss,
|
||||
errorContains: "user id is missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := r.refreshSessions(ctx, tc.u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(tc.sess)))
|
||||
if tc.errorContains == "" {
|
||||
assert.NoError(t, err)
|
||||
rw := db.New(s)
|
||||
var got []*Session
|
||||
require.NoError(t, rw.SearchWhere(ctx, &got, "true", nil))
|
||||
assert.Len(t, got, tc.wantCount)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tc.errorContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_ListSessions(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.ListSessions(ctx, "")
|
||||
assert.Nil(t, l)
|
||||
assert.ErrorContains(t, err, "auth token id is missing")
|
||||
})
|
||||
|
||||
ss := []*sessions.Session{
|
||||
{
|
||||
Id: "ttcp_1",
|
||||
Status: "status1",
|
||||
Endpoint: "address1",
|
||||
Type: "tcp",
|
||||
},
|
||||
{
|
||||
Id: "ttcp_2",
|
||||
Status: "status2",
|
||||
Endpoint: "address2",
|
||||
Type: "tcp",
|
||||
},
|
||||
{
|
||||
Id: "ttcp_3",
|
||||
Status: "status3",
|
||||
Endpoint: "address3",
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.refreshSessions(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(ss))))
|
||||
|
||||
t.Run("wrong user gets no sessions", func(t *testing.T) {
|
||||
l, err := r.ListSessions(ctx, kt2.AuthTokenId)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, l)
|
||||
})
|
||||
t.Run("correct token gets sessions", func(t *testing.T) {
|
||||
l, err := r.ListSessions(ctx, kt1.AuthTokenId)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, l, len(ss))
|
||||
assert.ElementsMatch(t, l, ss)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_QuerySessions(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 := "status % status1 or status % status2"
|
||||
|
||||
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.QuerySessions(ctx, tc.t, tc.query)
|
||||
assert.Nil(t, l)
|
||||
assert.ErrorContains(t, err, tc.errContains)
|
||||
})
|
||||
}
|
||||
|
||||
ss := []*sessions.Session{
|
||||
{
|
||||
Id: "ttcp_1",
|
||||
Status: "status1",
|
||||
Endpoint: "address1",
|
||||
Type: "tcp",
|
||||
},
|
||||
{
|
||||
Id: "ttcp_2",
|
||||
Status: "status2",
|
||||
Endpoint: "address2",
|
||||
Type: "tcp",
|
||||
},
|
||||
{
|
||||
Id: "ttcp_3",
|
||||
Status: "status3",
|
||||
Endpoint: "address3",
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.refreshSessions(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(ss))))
|
||||
|
||||
t.Run("wrong token gets no sessions", func(t *testing.T) {
|
||||
l, err := r.QuerySessions(ctx, kt2.AuthTokenId, query)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, l)
|
||||
})
|
||||
t.Run("correct token gets sessions", func(t *testing.T) {
|
||||
l, err := r.QuerySessions(ctx, kt1.AuthTokenId, query)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, l, 2)
|
||||
assert.ElementsMatch(t, l, ss[0:2])
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultSessionRetrievalFunc(t *testing.T) {
|
||||
tc := controller.NewTestController(t, nil)
|
||||
tc.Client().SetToken(tc.Token().Token)
|
||||
tarClient := targets.NewClient(tc.Client())
|
||||
_ = worker.NewTestWorker(t, &worker.TestWorkerOpts{
|
||||
InitialUpstreams: tc.ClusterAddrs(),
|
||||
WorkerAuthKms: tc.Config().WorkerAuthKms,
|
||||
})
|
||||
|
||||
tar1, err := tarClient.Create(tc.Context(), "tcp", "p_1234567890", targets.WithName("tar1"), targets.WithTcpTargetDefaultPort(1), targets.WithAddress("address"))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tar1)
|
||||
_, err = tarClient.AuthorizeSession(tc.Context(), tar1.Item.Id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, err := defaultSessionFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, got, 1)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// 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/targets"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/boundary/internal/types/resource"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/mql"
|
||||
)
|
||||
|
||||
// TargetRetrievalFunc is a function that retrieves targets
|
||||
// from the provided boundary addr using the provided token.
|
||||
type TargetRetrievalFunc func(ctx context.Context, addr, token string) ([]*targets.Target, error)
|
||||
|
||||
func defaultTargetFunc(ctx context.Context, addr, token string) ([]*targets.Target, error) {
|
||||
const op = "cache.defaultTargetFunc"
|
||||
client, err := api.NewClient(&api.Config{
|
||||
Addr: addr,
|
||||
Token: token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
tarClient := targets.NewClient(client)
|
||||
l, err := tarClient.List(ctx, "global", targets.WithRecursive(true))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return l.Items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[AuthToken]string, opt ...Option) error {
|
||||
const op = "cache.(Repository).refreshTargets"
|
||||
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")
|
||||
}
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if opts.withTargetRetrievalFunc == nil {
|
||||
opts.withTargetRetrievalFunc = defaultTargetFunc
|
||||
}
|
||||
|
||||
// Find and use a token for retrieving targets
|
||||
var gotResponse bool
|
||||
var resp []*targets.Target
|
||||
var retErr error
|
||||
for at, t := range tokens {
|
||||
resp, err = opts.withTargetRetrievalFunc(ctx, u.Address, t)
|
||||
if err != nil {
|
||||
// TODO: If we get an error about the token no longer having
|
||||
// permissions, remove it.
|
||||
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(ctx, u, resource.Target.String(), retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
if !gotResponse {
|
||||
return retErr
|
||||
}
|
||||
|
||||
event.WriteSysEvent(ctx, op, fmt.Sprintf("updating %d targets for user %v", len(resp), u))
|
||||
_, err = r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
// TODO: Instead of deleting everything, use refresh tokens and apply the delta
|
||||
if _, err := w.Exec(ctx, "delete from target where user_id = @user_id",
|
||||
[]any{sql.Named("user_id", u.Id)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range resp {
|
||||
item, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newTarget := &Target{
|
||||
UserId: u.Id,
|
||||
Id: t.Id,
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
Address: t.Address,
|
||||
Item: string(item),
|
||||
}
|
||||
onConflict := db.OnConflict{
|
||||
Target: db.Columns{"user_id", "id"},
|
||||
Action: db.UpdateAll(true),
|
||||
}
|
||||
if err := w.Create(ctx, newTarget, db.WithOnConflict(&onConflict)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListTargets(ctx context.Context, authTokenId string) ([]*targets.Target, error) {
|
||||
const op = "cache.(Repository).ListTargets"
|
||||
switch {
|
||||
case authTokenId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is missing")
|
||||
}
|
||||
ret, err := r.searchTargets(ctx, authTokenId, "true", nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *Repository) QueryTargets(ctx context.Context, authTokenId, query string) ([]*targets.Target, error) {
|
||||
const op = "cache.(Repository).QueryTargets"
|
||||
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, Target{}, mql.WithIgnoredFields("UserId", "Item"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
|
||||
}
|
||||
ret, err := r.searchTargets(ctx, authTokenId, w.Condition, w.Args)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *Repository) searchTargets(ctx context.Context, authTokenId, condition string, searchArgs []any) ([]*targets.Target, error) {
|
||||
const op = "cache.(Repository).searchTargets"
|
||||
switch {
|
||||
case authTokenId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is missing")
|
||||
case condition == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "condition is missing")
|
||||
}
|
||||
|
||||
condition = fmt.Sprintf("%s and user_id in (select user_id from auth_token where id = ?)", condition)
|
||||
args := append(searchArgs, authTokenId)
|
||||
var cachedTargets []*Target
|
||||
if err := r.rw.SearchWhere(ctx, &cachedTargets, condition, args, db.WithLimit(-1)); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
retTargets := make([]*targets.Target, 0, len(cachedTargets))
|
||||
for _, cachedTar := range cachedTargets {
|
||||
var tar targets.Target
|
||||
if err := json.Unmarshal([]byte(cachedTar.Item), &tar); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
retTargets = append(retTargets, &tar)
|
||||
}
|
||||
return retTargets, nil
|
||||
}
|
||||
|
||||
type Target struct {
|
||||
UserId string `gorm:"primaryKey"`
|
||||
Id string `gorm:"primaryKey"`
|
||||
Name string `gorm:"default:null"`
|
||||
Description string `gorm:"default:null"`
|
||||
Address string `gorm:"default:null"`
|
||||
Item string `gorm:"default:null"`
|
||||
}
|
||||
|
||||
func (*Target) TableName() string {
|
||||
return "target"
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
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"
|
||||
)
|
||||
|
||||
func TestRepository_refreshTargets(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: "k", TokenName: "t", AuthTokenId: at.Id}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{"k", "t"}: 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))
|
||||
|
||||
ts := []*targets.Target{
|
||||
{
|
||||
Id: "ttcp_1",
|
||||
Name: "name1",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 111,
|
||||
},
|
||||
{
|
||||
Id: "ttcp_2",
|
||||
Name: "name2",
|
||||
Address: "address2",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 222,
|
||||
},
|
||||
{
|
||||
Id: "ttcp_3",
|
||||
Name: "name3",
|
||||
Address: "address3",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 333,
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
u *user
|
||||
targets []*targets.Target
|
||||
wantCount int
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
u: &user{
|
||||
Id: at.UserId,
|
||||
Address: addr,
|
||||
},
|
||||
targets: ts,
|
||||
wantCount: len(ts),
|
||||
},
|
||||
{
|
||||
name: "repeated target with different values",
|
||||
u: &user{
|
||||
Address: addr,
|
||||
Id: at.UserId,
|
||||
},
|
||||
targets: append(ts, &targets.Target{
|
||||
Id: ts[0].Id,
|
||||
Name: "a different name",
|
||||
}),
|
||||
wantCount: len(ts),
|
||||
},
|
||||
{
|
||||
name: "nil user",
|
||||
u: nil,
|
||||
targets: ts,
|
||||
errorContains: "user is nil",
|
||||
},
|
||||
{
|
||||
name: "missing user Id",
|
||||
u: &user{
|
||||
Address: addr,
|
||||
},
|
||||
targets: ts,
|
||||
errorContains: "user id is missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := r.refreshTargets(ctx, tc.u, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(tc.targets)))
|
||||
if tc.errorContains == "" {
|
||||
assert.NoError(t, err)
|
||||
rw := db.New(s)
|
||||
var got []*Target
|
||||
require.NoError(t, rw.SearchWhere(ctx, &got, "true", nil))
|
||||
assert.Len(t, got, tc.wantCount)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tc.errorContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_ListTargets(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("token is missing", func(t *testing.T) {
|
||||
l, err := r.ListTargets(ctx, "")
|
||||
assert.Nil(t, l)
|
||||
assert.ErrorContains(t, err, "auth token id is missing")
|
||||
})
|
||||
|
||||
ts := []*targets.Target{
|
||||
{
|
||||
Id: "ttcp_1",
|
||||
Name: "name1",
|
||||
Address: "address1",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 111,
|
||||
},
|
||||
{
|
||||
Id: "ttcp_2",
|
||||
Name: "name2",
|
||||
Address: "address2",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 222,
|
||||
},
|
||||
{
|
||||
Id: "ttcp_3",
|
||||
Name: "name3",
|
||||
Address: "address3",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 333,
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.refreshTargets(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(ts))))
|
||||
|
||||
t.Run("wrong user gets no targets", func(t *testing.T) {
|
||||
l, err := r.ListTargets(ctx, kt2.AuthTokenId)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, l)
|
||||
})
|
||||
t.Run("correct token gets targets", func(t *testing.T) {
|
||||
l, err := r.ListTargets(ctx, kt1.AuthTokenId)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, l, len(ts))
|
||||
assert.ElementsMatch(t, l, ts)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_QueryTargets(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 := "name % name1 or name % name2"
|
||||
|
||||
errorCases := []struct {
|
||||
name string
|
||||
p string
|
||||
query string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "auth token id is missing",
|
||||
p: "",
|
||||
query: query,
|
||||
errContains: "auth token id is missing",
|
||||
},
|
||||
{
|
||||
name: "query is missing",
|
||||
p: "authtokenid",
|
||||
errContains: "query is missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range errorCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
l, err := r.QueryTargets(ctx, tc.p, tc.query)
|
||||
assert.Nil(t, l)
|
||||
assert.ErrorContains(t, err, tc.errContains)
|
||||
})
|
||||
}
|
||||
|
||||
ts := []*targets.Target{
|
||||
{
|
||||
Id: "ttcp_1",
|
||||
Name: "name1",
|
||||
Address: "address1",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 111,
|
||||
},
|
||||
{
|
||||
Id: "ttcp_2",
|
||||
Name: "name2",
|
||||
Address: "address2",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 222,
|
||||
},
|
||||
{
|
||||
Id: "ttcp_3",
|
||||
Name: "name3",
|
||||
Address: "address3",
|
||||
Type: "tcp",
|
||||
SessionMaxSeconds: 333,
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.refreshTargets(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(ts))))
|
||||
|
||||
t.Run("wrong token gets no targets", func(t *testing.T) {
|
||||
l, err := r.QueryTargets(ctx, kt2.AuthTokenId, query)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, l)
|
||||
})
|
||||
t.Run("correct token gets targets", func(t *testing.T) {
|
||||
l, err := r.QueryTargets(ctx, kt1.AuthTokenId, query)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, l, 2)
|
||||
assert.ElementsMatch(t, l, ts[0:2])
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultTargetRetrievalFunc(t *testing.T) {
|
||||
tc := controller.NewTestController(t, nil)
|
||||
tc.Client().SetToken(tc.Token().Token)
|
||||
tarClient := targets.NewClient(tc.Client())
|
||||
|
||||
tar1, err := tarClient.Create(tc.Context(), "tcp", "p_1234567890", targets.WithName("tar1"), targets.WithTcpTargetDefaultPort(1))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tar1)
|
||||
tar2, err := tarClient.Create(tc.Context(), "tcp", "p_1234567890", targets.WithName("tar2"), targets.WithTcpTargetDefaultPort(2))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tar2)
|
||||
|
||||
got, err := defaultTargetFunc(tc.Context(), tc.ApiAddrs()[0], tc.Token().Token)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, got, tar1.Item)
|
||||
assert.Contains(t, got, tar2.Item)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdErrors "errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ringToken is a test struct used to group a keyring type and token name
|
||||
// so it can be used in an authtoken lookup function.
|
||||
type ringToken struct {
|
||||
k string
|
||||
t string
|
||||
}
|
||||
|
||||
// mapBasedAuthTokenKeyringLookup provides a fake KeyringTokenLookupFn that uses
|
||||
// the provided map to perform lookups for the tokens
|
||||
func mapBasedAuthTokenKeyringLookup(m map[ringToken]*authtokens.AuthToken) KeyringTokenLookupFn {
|
||||
return func(k, t string) *authtokens.AuthToken {
|
||||
return m[ringToken{k, t}]
|
||||
}
|
||||
}
|
||||
|
||||
// sliceBasedAuthTokenBoundaryReader provides a fake BoundaryTokenReaderFn that uses
|
||||
// the provided map to lookup an auth tokens information.
|
||||
func sliceBasedAuthTokenBoundaryReader(s []*authtokens.AuthToken) BoundaryTokenReaderFn {
|
||||
return func(ctx context.Context, addr, at string) (*authtokens.AuthToken, error) {
|
||||
for _, v := range s {
|
||||
if at == v.Token {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return nil, stdErrors.New("not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_SaveError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := NewRepository(ctx, s, &sync.Map{},
|
||||
mapBasedAuthTokenKeyringLookup(map[ringToken]*authtokens.AuthToken{}),
|
||||
sliceBasedAuthTokenBoundaryReader(nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
testResource := "test_resource_type"
|
||||
testErr := fmt.Errorf("test error for %q", testResource)
|
||||
|
||||
u := &user{
|
||||
Id: "u1",
|
||||
Address: "addr",
|
||||
}
|
||||
require.NoError(t, r.rw.Create(ctx, u))
|
||||
|
||||
t.Run("empty resource type", func(t *testing.T) {
|
||||
assert.ErrorContains(t, r.SaveError(ctx, u, "", testErr), "resource type is empty")
|
||||
})
|
||||
t.Run("nil error", func(t *testing.T) {
|
||||
assert.ErrorContains(t, r.SaveError(ctx, u, testResource, nil), "error is nil")
|
||||
})
|
||||
t.Run("nil user", func(t *testing.T) {
|
||||
assert.ErrorContains(t, r.SaveError(ctx, nil, testResource, testErr), "user is nil")
|
||||
})
|
||||
t.Run("success", func(t *testing.T) {
|
||||
assert.NoError(t, r.SaveError(ctx, u, testResource, testErr))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// AuthTokenIdSegmentCount are the number of segments, delineated by "_", that
|
||||
// make up the auth token id inside an auth token.
|
||||
// For example, an authtoken format should look something like at_1234567890_sometokenpayload
|
||||
const AuthTokenIdSegmentCount = 2
|
||||
|
||||
// upsertUserAndAuthToken upserts a user and authToken using the data in the provided authtoken.
|
||||
// If creating this user results in the number of users exceeding the limit of
|
||||
// allowed users it deletes the oldest one.
|
||||
func upsertUserAndAuthToken(ctx context.Context, reader db.Reader, writer db.Writer, bAddr string, at *authtokens.AuthToken) error {
|
||||
const op = "cache.upsertUserAndAuthToken"
|
||||
switch {
|
||||
case util.IsNil(reader):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "reader is nil")
|
||||
case util.IsNil(writer):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
|
||||
case !writer.IsTx(ctx):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "writer isn't part of an inflight transaction")
|
||||
case util.IsNil(at):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "auth token is nil")
|
||||
case bAddr == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "boundary address is empty")
|
||||
case at.Id == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "auth token id is empty")
|
||||
case at.UserId == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "auth token user id is empty")
|
||||
}
|
||||
{
|
||||
// always make sure the user exists when adding a token
|
||||
u := &user{
|
||||
Id: at.UserId,
|
||||
Address: bAddr,
|
||||
}
|
||||
onConflict := &db.OnConflict{
|
||||
Target: db.Columns{"id"},
|
||||
Action: db.DoNothing(true),
|
||||
}
|
||||
if err := writer.Create(ctx, u, db.WithOnConflict(onConflict)); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
st := &AuthToken{
|
||||
Id: at.Id,
|
||||
UserId: at.UserId,
|
||||
LastAccessedTime: time.Now(),
|
||||
ExpirationTime: at.ExpirationTime,
|
||||
}
|
||||
onConflict := &db.OnConflict{
|
||||
Target: db.Columns{"id"},
|
||||
Action: db.SetColumns([]string{"last_accessed_time", "expiration_time"}),
|
||||
}
|
||||
if err := writer.Create(ctx, st, db.WithOnConflict(onConflict)); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
|
||||
var users []*user
|
||||
if err := reader.SearchWhere(ctx, &users, "true", []any{}, db.WithLimit(-1)); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if len(users) <= usersLimit {
|
||||
return nil
|
||||
}
|
||||
|
||||
var oldestUser *user
|
||||
var oldestUsersTime *time.Time
|
||||
for _, u := range users {
|
||||
ats, err := listTokens(ctx, reader, u)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
for _, at := range ats {
|
||||
if oldestUsersTime == nil || oldestUsersTime.After(at.LastAccessedTime) {
|
||||
oldestUser = u
|
||||
oldestUsersTime = &at.LastAccessedTime
|
||||
}
|
||||
}
|
||||
}
|
||||
if oldestUser != nil {
|
||||
if _, err := deleteUser(ctx, writer, oldestUser); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRawToken upserts the auth token's user and auth token in the db and
|
||||
// stores the actual raw auth token in the repositories in memory storage.
|
||||
// The raw token must be valid and present in boundary and be for a user that
|
||||
// has permission to send a Read request for itself to boundary.
|
||||
func (r *Repository) AddRawToken(ctx context.Context, bAddr string, rawToken string) error {
|
||||
const op = "cache.(Repository).AddRawToken"
|
||||
switch {
|
||||
case rawToken == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "boundary auth token is empty")
|
||||
case bAddr == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "boundary address is empty")
|
||||
}
|
||||
|
||||
// rawToken should look something like at_1234567890_someencryptedpayload
|
||||
atIdParts := strings.SplitN(rawToken, "_", 4)
|
||||
if len(atIdParts) != 3 {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "boundary auth token is is malformed")
|
||||
}
|
||||
atId := strings.Join(atIdParts[:AuthTokenIdSegmentCount], "_")
|
||||
|
||||
var at *authtokens.AuthToken
|
||||
{
|
||||
var inMemAuthToken *authtokens.AuthToken
|
||||
atV, inMem := r.idToKeyringlessAuthToken.Load(atId)
|
||||
if inMem {
|
||||
var ok bool
|
||||
inMemAuthToken, ok = atV.(*authtokens.AuthToken)
|
||||
if !ok {
|
||||
return errors.New(ctx, errors.Internal, op, "unable to cast in memory auth token to *authtoken.AuthToken")
|
||||
}
|
||||
}
|
||||
t := &AuthToken{
|
||||
Id: atId,
|
||||
}
|
||||
err := r.rw.LookupById(ctx, t)
|
||||
switch {
|
||||
case err != nil && !errors.IsNotFoundError(err):
|
||||
return errors.Wrap(ctx, err, op)
|
||||
case errors.IsNotFoundError(err) || !inMem:
|
||||
// if we don't know about it in the cache or we don't know about
|
||||
// this auth token in memory, get it from boundary to sure up the
|
||||
// cache information about this auth token.
|
||||
at, err = r.tokenReadFromBoundaryFn(ctx, bAddr, rawToken)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
case t.UserId != inMemAuthToken.UserId:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user id doesn't match what is specified in the stored auth token")
|
||||
case inMem:
|
||||
at = inMemAuthToken
|
||||
}
|
||||
}
|
||||
|
||||
if at != nil {
|
||||
// The token is never returned from boundary except in the original auth
|
||||
// request so we must set the token.
|
||||
at.Token = rawToken
|
||||
}
|
||||
|
||||
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error {
|
||||
if err := upsertUserAndAuthToken(ctx, reader, writer, bAddr, at); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
r.idToKeyringlessAuthToken.Store(at.Id, at)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddKeyringToken adds a token to the repository. If the token's id in the
|
||||
// keyring doesn't match the id provided an error is returned.
|
||||
// The token must be valid and present in boundary and be for a user that
|
||||
// has permission to send a self-Read request to boundary. The user id
|
||||
// stored in the keyring must also match the user id returned from boundary.
|
||||
func (r *Repository) AddKeyringToken(ctx context.Context, bAddr string, token KeyringToken) error {
|
||||
const op = "cache.(Repository).AddKeyringToken"
|
||||
switch {
|
||||
case token.TokenName == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "token name is empty")
|
||||
case token.KeyringType == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "keyring type is empty")
|
||||
case token.AuthTokenId == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "boundary auth token id is empty")
|
||||
case bAddr == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "boundary address is empty")
|
||||
}
|
||||
kt := token.clone()
|
||||
keyringStoredAt := r.tokenKeyringFn(kt.KeyringType, kt.TokenName)
|
||||
if keyringStoredAt == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "unable to find token in the keyring specified")
|
||||
}
|
||||
if kt.AuthTokenId != keyringStoredAt.Id {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "provided auth token id doesn't match the one stored")
|
||||
}
|
||||
|
||||
var at *authtokens.AuthToken
|
||||
{
|
||||
cachedAt := &AuthToken{
|
||||
Id: kt.AuthTokenId,
|
||||
}
|
||||
err := r.rw.LookupById(ctx, cachedAt)
|
||||
switch {
|
||||
case err != nil && !errors.IsNotFoundError(err):
|
||||
return errors.Wrap(ctx, err, op)
|
||||
case errors.IsNotFoundError(err):
|
||||
at, err = r.tokenReadFromBoundaryFn(ctx, bAddr, keyringStoredAt.Token)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
case cachedAt.UserId != keyringStoredAt.UserId:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user id doesn't match what is specified in the stored auth token")
|
||||
default:
|
||||
at = keyringStoredAt
|
||||
}
|
||||
}
|
||||
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error {
|
||||
if err := upsertUserAndAuthToken(ctx, reader, writer, bAddr, at); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
onConflict := &db.OnConflict{
|
||||
Target: db.Columns{"keyring_type", "token_name"},
|
||||
Action: db.SetColumns([]string{"auth_token_id"}),
|
||||
}
|
||||
if err := writer.Create(ctx, kt, db.WithOnConflict(onConflict)); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupToken returns the Token in the cache if one exists.
|
||||
// Accepts withUpdateLastAccessedTime options. If withUpdateLastAccessedTime
|
||||
// is provided, the last update time of the returned token will be updated to
|
||||
// the current time and reflected in the db. The returned AuthToken will not
|
||||
// have the updated time.
|
||||
func (r *Repository) LookupToken(ctx context.Context, authTokenId string, opt ...Option) (*AuthToken, error) {
|
||||
const op = "cache.(Repository).LookupToken"
|
||||
switch {
|
||||
case authTokenId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is empty")
|
||||
}
|
||||
|
||||
at := &AuthToken{
|
||||
Id: authTokenId,
|
||||
}
|
||||
if err := r.rw.LookupById(ctx, at); err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
if opts.withUpdateLastAccessedTime {
|
||||
updatedT := &AuthToken{
|
||||
Id: authTokenId,
|
||||
LastAccessedTime: time.Now(),
|
||||
}
|
||||
if _, err := r.rw.Update(ctx, updatedT, []string{"LastAccessedTime"}, nil); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
return at, nil
|
||||
}
|
||||
|
||||
// deleteKeyringToken deletes a keyring token
|
||||
func (r *Repository) deleteKeyringToken(ctx context.Context, kt KeyringToken) error {
|
||||
const op = "cache.(Repository).deleteKeyringToken"
|
||||
switch {
|
||||
case kt.KeyringType == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing keyring type")
|
||||
case kt.TokenName == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "token name type")
|
||||
}
|
||||
|
||||
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error {
|
||||
// TODO(https://github.com/go-gorm/gorm/issues/4879): Use the
|
||||
// writer.Delete() function once the gorm bug is fixed. Until then
|
||||
// the gorm driver for sqlite has an error which wont execute a
|
||||
// delete correctly. as a work around we manually execute the
|
||||
// query here.
|
||||
n, err := writer.Exec(ctx, "delete from keyring_token where (keyring_type, token_name) = (?, ?)", []any{kt.KeyringType, kt.TokenName})
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
switch n {
|
||||
case 1:
|
||||
if err := cleanExpiredOrOrphanedAuthTokens(ctx, writer, r.idToKeyringlessAuthToken); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
case 0:
|
||||
return errors.New(ctx, errors.RecordNotFound, op, "token not found when attempting deletion")
|
||||
default:
|
||||
return errors.New(ctx, errors.MultipleRecords, op, "multiple tokens deleted when one was requested")
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanExpiredOrOrphanedAuthTokens removes all tokens which are older than the staleness limit
|
||||
// or does not have either a keyring or keyringless reference to it.
|
||||
func (r *Repository) cleanExpiredOrOrphanedAuthTokens(ctx context.Context) error {
|
||||
const op = "cache.Repository.cleanExpiredOrOrphanedAuthTokens"
|
||||
_, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error {
|
||||
if err := cleanExpiredOrOrphanedAuthTokens(ctx, writer, r.idToKeyringlessAuthToken); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// cleanExpiredOrOrphanedAuthTokens removes all tokens which are older than the staleness limit
|
||||
// or does not have either a keyring or keyringless reference to it.
|
||||
func cleanExpiredOrOrphanedAuthTokens(ctx context.Context, writer db.Writer, idToKeyringlessAuthToken *sync.Map) error {
|
||||
const op = "cache.cleanExpiredOrOrphanedAuthTokens"
|
||||
switch {
|
||||
case util.IsNil(writer):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
|
||||
case idToKeyringlessAuthToken == nil:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "keyringless auth token map is nil")
|
||||
case !writer.IsTx(ctx):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "writer isn't part of an inflight transaction")
|
||||
}
|
||||
|
||||
var keyringlessAuthTokens []string
|
||||
idToKeyringlessAuthToken.Range(func(key, _ any) bool {
|
||||
keyringlessAuthTokens = append(keyringlessAuthTokens, key.(string))
|
||||
return true
|
||||
})
|
||||
|
||||
deleteOrphanedAuthTokens := `
|
||||
delete from auth_token
|
||||
where
|
||||
last_accessed_time < @last_accessed_time
|
||||
or
|
||||
-- sqlite stores expiration_time as a string in a format that might not match
|
||||
-- what is being used by current_timestamp. Using datetime() makes the
|
||||
-- formats match which allow the string comparison performed here to work
|
||||
-- the same as a time comparison.
|
||||
datetime(expiration_time) < current_timestamp
|
||||
or
|
||||
%s
|
||||
`
|
||||
args := []any{sql.Named("last_accessed_time", time.Now().Add(-tokenStalenessLimit))}
|
||||
|
||||
idInSection := "id not in (select auth_token_id from keyring_token)"
|
||||
if len(keyringlessAuthTokens) > 0 {
|
||||
// Note: We have to build the statement like this because if the slice of string
|
||||
// is empty this gets converted to a query that says " and id not in (NULL)"
|
||||
idInSection = fmt.Sprintf("(%s and id not in @keyringless_token_ids)", idInSection)
|
||||
args = append(args, sql.Named("keyringless_token_ids", keyringlessAuthTokens))
|
||||
}
|
||||
|
||||
if _, err := writer.Exec(ctx, fmt.Sprintf(deleteOrphanedAuthTokens, idInSection), args); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listUsers returns all known tokens in the cache
|
||||
func (r *Repository) listUsers(ctx context.Context) ([]*user, error) {
|
||||
const op = "cache.(Repository).listUsers"
|
||||
var ret []*user
|
||||
if err := r.rw.SearchWhere(ctx, &ret, "true", nil); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// listTokens returns all known tokens in the cache for the provided user
|
||||
func (r *Repository) listTokens(ctx context.Context, u *user) ([]*AuthToken, error) {
|
||||
const op = "cache.(Repository).listTokens"
|
||||
ret, err := listTokens(ctx, r.rw, u)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// listTokens returns all known tokens in the cache for the provided user using the provided reader
|
||||
func listTokens(ctx context.Context, reader db.Reader, u *user) ([]*AuthToken, error) {
|
||||
const op = "cache.listTokens"
|
||||
switch {
|
||||
case util.IsNil(u):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is missing")
|
||||
}
|
||||
var ret []*AuthToken
|
||||
if err := reader.SearchWhere(ctx, &ret, "user_id = ?", []any{u.Id}); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// listKeyringTokens returns all known keyring tokens in the cache for the provided auth token
|
||||
func (r *Repository) listKeyringTokens(ctx context.Context, at *AuthToken) ([]*KeyringToken, error) {
|
||||
const op = "cache.(Repository).listTokens"
|
||||
switch {
|
||||
case util.IsNil(at):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token is nil")
|
||||
case at.Id == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token id is empty")
|
||||
}
|
||||
var ret []*KeyringToken
|
||||
if err := r.rw.SearchWhere(ctx, &ret, "auth_token_id = ?", []any{at.Id}); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// syncKeyringlessTokensWithDb removes the in memory storage of auth tokens if
|
||||
// they are no longer represented in the db.
|
||||
func (r *Repository) syncKeyringlessTokensWithDb(ctx context.Context) error {
|
||||
const op = "cache.(Repository).syncKeyringlessTokensWithDb"
|
||||
var ret []*AuthToken
|
||||
if err := r.rw.SearchWhere(ctx, &ret, "true", nil); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
authTokenIds := make(map[string]struct{})
|
||||
for _, at := range ret {
|
||||
authTokenIds[at.Id] = struct{}{}
|
||||
}
|
||||
r.idToKeyringlessAuthToken.Range(func(key, value any) bool {
|
||||
k := key.(string)
|
||||
if _, ok := authTokenIds[k]; !ok {
|
||||
r.idToKeyringlessAuthToken.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteUser executes a delete command using the provided db.Writer for the provided user.
|
||||
func deleteUser(ctx context.Context, w db.Writer, u *user) (int, error) {
|
||||
const op = "cache.deleteUser"
|
||||
switch {
|
||||
case util.IsNil(w):
|
||||
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
|
||||
case u.Id == "":
|
||||
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing id")
|
||||
}
|
||||
// TODO(https://github.com/go-gorm/gorm/issues/4879): Use the
|
||||
// writer.Delete() function once the gorm bug is fixed. Until then
|
||||
// the gorm driver for sqlite has an error which wont execute a
|
||||
// delete correctly. as a work around we manually execute the
|
||||
// query here.
|
||||
n, err := w.Exec(ctx, "delete from user where id = ?", []any{u.Id})
|
||||
if err != nil {
|
||||
err = errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// user is a gorm model for the user table. It represents a user
|
||||
type user struct {
|
||||
Id string `gorm:"primaryKey"`
|
||||
Address string `gorm:"default:null"`
|
||||
}
|
||||
|
||||
func (*user) TableName() string {
|
||||
return "user"
|
||||
}
|
||||
|
||||
func (u *user) clone() *user {
|
||||
return &user{
|
||||
Id: u.Id,
|
||||
Address: u.Address,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthToken is a gorm model for the token table and is an auth token known by the
|
||||
// boundary client side cache daemon.
|
||||
type AuthToken struct {
|
||||
Id string `gorm:"primaryKey"`
|
||||
UserId string `gorm:"default:null"`
|
||||
LastAccessedTime time.Time `gorm:"default:(strftime('%Y-%m-%d %H:%M:%f','now'))"`
|
||||
ExpirationTime time.Time
|
||||
}
|
||||
|
||||
func (*AuthToken) TableName() string {
|
||||
return "auth_token"
|
||||
}
|
||||
|
||||
// KeyringToken is a gorm model for the keyring stored token
|
||||
type KeyringToken struct {
|
||||
KeyringType string `gorm:"primaryKey"`
|
||||
TokenName string `gorm:"primaryKey"`
|
||||
AuthTokenId string `gorm:"default:null"`
|
||||
}
|
||||
|
||||
func (*KeyringToken) TableName() string {
|
||||
return "keyring_token"
|
||||
}
|
||||
|
||||
func (kt *KeyringToken) clone() *KeyringToken {
|
||||
return &KeyringToken{
|
||||
KeyringType: kt.KeyringType,
|
||||
TokenName: kt.TokenName,
|
||||
AuthTokenId: kt.AuthTokenId,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,822 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
func TestRepository_AddKeyringToken(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,
|
||||
}
|
||||
// used to test mismatched user ids between keyring and cache
|
||||
mismatchingAt := &authtokens.AuthToken{
|
||||
Id: "at_mismatch",
|
||||
Token: "at_mismatch_token",
|
||||
UserId: u.Id,
|
||||
}
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at, mismatchingAt}
|
||||
keyring := "k"
|
||||
tokenName := "t"
|
||||
atMap := make(map[ringToken]*authtokens.AuthToken)
|
||||
atMap[ringToken{keyring, tokenName}] = at
|
||||
atMap[ringToken{"mismatch", "mismatch"}] = mismatchingAt
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("userid mismatch between db and keyring", func(t *testing.T) {
|
||||
require.NoError(t, r.AddKeyringToken(ctx, "address", KeyringToken{
|
||||
KeyringType: "mismatch",
|
||||
TokenName: "mismatch",
|
||||
AuthTokenId: mismatchingAt.Id,
|
||||
}))
|
||||
|
||||
mismatchingAt.UserId = "changedToMismatch"
|
||||
assert.ErrorContains(t, r.AddKeyringToken(ctx, "address", KeyringToken{
|
||||
KeyringType: "mismatch",
|
||||
TokenName: "mismatch",
|
||||
AuthTokenId: mismatchingAt.Id,
|
||||
}), "user id doesn't match what is specified in the stored auth token")
|
||||
})
|
||||
|
||||
errCases := []struct {
|
||||
name string
|
||||
addr string
|
||||
kt KeyringToken
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
addr: "address",
|
||||
kt: KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: tokenName,
|
||||
AuthTokenId: at.Id,
|
||||
},
|
||||
errorContains: "",
|
||||
},
|
||||
{
|
||||
name: "not in keyring",
|
||||
addr: "address",
|
||||
kt: KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: "unknowntokenname",
|
||||
AuthTokenId: at.Id,
|
||||
},
|
||||
errorContains: "unable to find token in the keyring specified",
|
||||
},
|
||||
{
|
||||
name: "missing address",
|
||||
kt: KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: tokenName,
|
||||
AuthTokenId: at.Id,
|
||||
},
|
||||
errorContains: "boundary address is empty",
|
||||
},
|
||||
{
|
||||
name: "missing token",
|
||||
addr: "address",
|
||||
kt: KeyringToken{
|
||||
KeyringType: keyring,
|
||||
AuthTokenId: at.Id,
|
||||
},
|
||||
errorContains: "token name is empty",
|
||||
},
|
||||
{
|
||||
name: "missing keyring type",
|
||||
addr: "address",
|
||||
kt: KeyringToken{
|
||||
TokenName: tokenName,
|
||||
AuthTokenId: at.Id,
|
||||
},
|
||||
errorContains: "keyring type is empty",
|
||||
},
|
||||
{
|
||||
name: "missing auth token id",
|
||||
addr: "address",
|
||||
kt: KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: tokenName,
|
||||
},
|
||||
errorContains: "boundary auth token id is empty",
|
||||
},
|
||||
{
|
||||
name: "unmatching auth token id",
|
||||
addr: "address",
|
||||
kt: KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: tokenName,
|
||||
AuthTokenId: "wrong auth token id",
|
||||
},
|
||||
errorContains: "provided auth token id doesn't match",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range errCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := r.AddKeyringToken(ctx, tc.addr, tc.kt)
|
||||
if tc.errorContains == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tc.errorContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_AddRawToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: "u1",
|
||||
}
|
||||
existingAt := &authtokens.AuthToken{
|
||||
Id: "at_existing",
|
||||
Token: "at_existing_token",
|
||||
UserId: "u2",
|
||||
}
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at, existingAt}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(map[ringToken]*authtokens.AuthToken{}), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("mismatched userid between memory and db", func(t *testing.T) {
|
||||
require.NoError(t, r.AddRawToken(ctx, "address", existingAt.Token))
|
||||
loadedExistingV, loaded := r.idToKeyringlessAuthToken.Load(existingAt.Id)
|
||||
require.True(t, loaded)
|
||||
loadedExistingAt := loadedExistingV.(*authtokens.AuthToken)
|
||||
loadedExistingAt.UserId = "mismatchingUserId"
|
||||
r.idToKeyringlessAuthToken.Store(existingAt.Id, loadedExistingAt)
|
||||
|
||||
err := r.AddRawToken(ctx, "address", loadedExistingAt.Token)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "user id doesn't match what is specified in the stored auth token")
|
||||
})
|
||||
|
||||
errCases := []struct {
|
||||
name string
|
||||
addr string
|
||||
rawAt string
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
addr: "address",
|
||||
rawAt: at.Token,
|
||||
errorContains: "",
|
||||
},
|
||||
{
|
||||
name: "missing address",
|
||||
rawAt: at.Token,
|
||||
errorContains: "boundary address is empty",
|
||||
},
|
||||
{
|
||||
name: "missing token",
|
||||
addr: "address",
|
||||
errorContains: "auth token is empty",
|
||||
},
|
||||
{
|
||||
name: "malformed auth token",
|
||||
addr: "address",
|
||||
rawAt: fmt.Sprintf("%s_extraunderscore", at.Token),
|
||||
errorContains: "boundary auth token is is malformed",
|
||||
},
|
||||
{
|
||||
name: "not found in boundary",
|
||||
addr: "address",
|
||||
rawAt: "at_123_notfoundinboundary",
|
||||
errorContains: "not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range errCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := r.AddRawToken(ctx, tc.addr, tc.rawAt)
|
||||
if tc.errorContains == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tc.errorContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_AddToken_EvictsOverLimitUsers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{
|
||||
{
|
||||
UserId: "user_base",
|
||||
Id: "at_base",
|
||||
Token: "at_base_token",
|
||||
},
|
||||
}
|
||||
for i := 0; i < usersLimit; i++ {
|
||||
iAt := &authtokens.AuthToken{
|
||||
UserId: fmt.Sprintf("user%d", i),
|
||||
Id: fmt.Sprintf("at_%d", i),
|
||||
}
|
||||
iAt.Token = fmt.Sprintf("%s_token", iAt.Id)
|
||||
boundaryAuthTokens = append(boundaryAuthTokens, iAt)
|
||||
}
|
||||
|
||||
atMap := make(map[ringToken]*authtokens.AuthToken)
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
kt := KeyringToken{
|
||||
KeyringType: "keyring",
|
||||
TokenName: "token",
|
||||
AuthTokenId: boundaryAuthTokens[0].Id,
|
||||
}
|
||||
atMap[ringToken{kt.KeyringType, kt.TokenName}] = boundaryAuthTokens[0]
|
||||
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
||||
|
||||
lastKtAdded := kt
|
||||
for i, at := range boundaryAuthTokens[1:] {
|
||||
kr := fmt.Sprintf("%s%d", kt.KeyringType, i)
|
||||
tn := fmt.Sprintf("%s%d", kt.TokenName, i)
|
||||
ikt := KeyringToken{KeyringType: kr, TokenName: tn, AuthTokenId: at.Id}
|
||||
|
||||
atMap[ringToken{ikt.KeyringType, ikt.TokenName}] = at
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, ikt))
|
||||
lastKtAdded = ikt
|
||||
}
|
||||
|
||||
// Lookup the first token added. It should have been deleted when the
|
||||
// associated user was evicted for being used the least recently.
|
||||
gotP, err := r.LookupToken(ctx, kt.AuthTokenId)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, gotP)
|
||||
|
||||
gotP, err = r.LookupToken(ctx, lastKtAdded.AuthTokenId)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, gotP)
|
||||
}
|
||||
|
||||
func TestRepository_AddToken_EvictsOverLimit_Keyringless(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{
|
||||
{
|
||||
UserId: "user_base",
|
||||
Id: "at_base",
|
||||
Token: "at_base_token",
|
||||
},
|
||||
}
|
||||
for i := 0; i < usersLimit; i++ {
|
||||
iAt := &authtokens.AuthToken{
|
||||
UserId: fmt.Sprintf("user%d", i),
|
||||
Id: fmt.Sprintf("at_%d", i),
|
||||
}
|
||||
iAt.Token = fmt.Sprintf("%s_token", iAt.Id)
|
||||
boundaryAuthTokens = append(boundaryAuthTokens, iAt)
|
||||
}
|
||||
|
||||
atMap := make(map[ringToken]*authtokens.AuthToken)
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
|
||||
assert.NoError(t, r.AddRawToken(ctx, addr, boundaryAuthTokens[0].Token))
|
||||
assert.NoError(t, r.AddRawToken(ctx, addr, boundaryAuthTokens[0].Token))
|
||||
for _, at := range boundaryAuthTokens[1:] {
|
||||
assert.NoError(t, r.AddRawToken(ctx, addr, at.Token))
|
||||
}
|
||||
// Lookup the first persona added. It should have been evicted from the db
|
||||
// for being used the least recently. It is only removed from the db once
|
||||
// cleanAuthTokens is called.
|
||||
gotP, err := r.LookupToken(ctx, boundaryAuthTokens[0].Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, gotP)
|
||||
_, ok := r.idToKeyringlessAuthToken.Load(boundaryAuthTokens[0].Id)
|
||||
assert.True(t, ok)
|
||||
|
||||
gotP, err = r.LookupToken(ctx, boundaryAuthTokens[len(boundaryAuthTokens)-1].Id)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, gotP)
|
||||
_, ok = r.idToKeyringlessAuthToken.Load(boundaryAuthTokens[len(boundaryAuthTokens)-1].Id)
|
||||
assert.True(t, ok)
|
||||
|
||||
assert.NoError(t, r.syncKeyringlessTokensWithDb(ctx))
|
||||
_, ok = r.idToKeyringlessAuthToken.Load(boundaryAuthTokens[0].Id)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestRepository_CleanAuthTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
at := &authtokens.AuthToken{
|
||||
UserId: "user_base",
|
||||
Id: "at_base",
|
||||
Token: "at_base_token",
|
||||
}
|
||||
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)
|
||||
assert.NoError(t, r.AddRawToken(ctx, "baddr", at.Token))
|
||||
_, present := r.idToKeyringlessAuthToken.Load(at.Id)
|
||||
assert.True(t, present)
|
||||
|
||||
_, err = r.rw.Delete(ctx, &user{Id: at.UserId})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, present = r.idToKeyringlessAuthToken.Load(at.Id)
|
||||
assert.True(t, present)
|
||||
|
||||
assert.NoError(t, r.syncKeyringlessTokensWithDb(ctx))
|
||||
|
||||
_, present = r.idToKeyringlessAuthToken.Load(at.Id)
|
||||
assert.False(t, present)
|
||||
}
|
||||
|
||||
func TestRepository_AddKeyringToken_AddingExistingUpdatesLastAccessedTime(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
at1 := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: "u_1",
|
||||
}
|
||||
kt1 := KeyringToken{
|
||||
TokenName: "t1",
|
||||
KeyringType: "k1",
|
||||
AuthTokenId: at1.Id,
|
||||
}
|
||||
at2 := &authtokens.AuthToken{
|
||||
Id: "at_2",
|
||||
Token: "at_2_token",
|
||||
UserId: "u_2",
|
||||
}
|
||||
kt2 := KeyringToken{
|
||||
TokenName: "t2",
|
||||
KeyringType: "k2",
|
||||
AuthTokenId: "at_2",
|
||||
}
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at1, at2}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{kt1.KeyringType, kt1.TokenName}: at1,
|
||||
{kt2.KeyringType, kt2.TokenName}: at2,
|
||||
}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt2))
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
|
||||
|
||||
gotP1, err := r.LookupToken(ctx, kt1.AuthTokenId)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gotP1)
|
||||
gotP2, err := r.LookupToken(ctx, kt2.AuthTokenId)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gotP2)
|
||||
|
||||
assert.Greater(t, gotP1.LastAccessedTime, gotP2.LastAccessedTime)
|
||||
}
|
||||
|
||||
func TestRepository_AddRawToken_AddingExistingUpdatesLastAccessedTime(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
at1 := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: "u_1",
|
||||
}
|
||||
at2 := &authtokens.AuthToken{
|
||||
Id: "at_2",
|
||||
Token: "at_2_token",
|
||||
UserId: "u_2",
|
||||
}
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at1, at2}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NoError(t, r.AddRawToken(ctx, addr, at1.Token))
|
||||
assert.NoError(t, r.AddRawToken(ctx, addr, at2.Token))
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
assert.NoError(t, r.AddRawToken(ctx, addr, at1.Token))
|
||||
|
||||
gotP1, err := r.LookupToken(ctx, at1.Id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gotP1)
|
||||
gotP2, err := r.LookupToken(ctx, at2.Id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gotP2)
|
||||
|
||||
assert.Greater(t, gotP1.LastAccessedTime, gotP2.LastAccessedTime)
|
||||
}
|
||||
|
||||
func TestRepository_ListTokens(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,
|
||||
}
|
||||
|
||||
atMap := make(map[ringToken]*authtokens.AuthToken)
|
||||
|
||||
ktTokenCount := 15
|
||||
for i := 0; i < ktTokenCount; i++ {
|
||||
k := fmt.Sprintf("k%d", i)
|
||||
t := fmt.Sprintf("t%d", i)
|
||||
atMap[ringToken{k, t}] = at
|
||||
}
|
||||
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
|
||||
require.NoError(t, err)
|
||||
|
||||
for k, v := range atMap {
|
||||
require.NoError(t, r.AddKeyringToken(ctx, addr, KeyringToken{
|
||||
KeyringType: k.k,
|
||||
TokenName: k.t,
|
||||
AuthTokenId: v.Id,
|
||||
}))
|
||||
}
|
||||
|
||||
t.Run("no token", func(t *testing.T) {
|
||||
gotP, err := r.listTokens(ctx, &user{Id: "tokenless"})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, gotP)
|
||||
})
|
||||
|
||||
t.Run("many tokens", func(t *testing.T) {
|
||||
gotAt, err := r.listTokens(ctx, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, gotAt, 1)
|
||||
|
||||
gotKrt, err := r.listKeyringTokens(ctx, gotAt[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, gotKrt, ktTokenCount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_DeleteKeyringToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
at1 := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: "u_1",
|
||||
}
|
||||
kt1 := KeyringToken{
|
||||
TokenName: "t1",
|
||||
KeyringType: "k1",
|
||||
AuthTokenId: at1.Id,
|
||||
}
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at1}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{kt1.KeyringType, kt1.TokenName}: at1,
|
||||
}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("delete non existing", func(t *testing.T) {
|
||||
assert.ErrorContains(t, r.deleteKeyringToken(ctx, KeyringToken{KeyringType: "Unknown", TokenName: "Unknown"}), "not found")
|
||||
})
|
||||
|
||||
t.Run("delete existing", func(t *testing.T) {
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
|
||||
p, err := r.LookupToken(ctx, kt1.AuthTokenId)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
|
||||
assert.NoError(t, r.deleteKeyringToken(ctx, kt1))
|
||||
|
||||
got, err := r.LookupToken(ctx, kt1.AuthTokenId)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_LookupToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "address"
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: "u_1",
|
||||
}
|
||||
kt := KeyringToken{
|
||||
TokenName: "t1",
|
||||
KeyringType: "k1",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{kt.KeyringType, kt.TokenName}: at,
|
||||
}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("empty token id", func(t *testing.T) {
|
||||
p, err := r.LookupToken(ctx, "")
|
||||
assert.ErrorContains(t, err, "auth token id is empty")
|
||||
assert.Nil(t, p)
|
||||
})
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
p, err := r.LookupToken(ctx, "token")
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, p)
|
||||
})
|
||||
t.Run("found", func(t *testing.T) {
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
||||
p, err := r.LookupToken(ctx, kt.AuthTokenId)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, p)
|
||||
})
|
||||
|
||||
t.Run("withUpdateLastAccessedTime", func(t *testing.T) {
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
beforeP, err := r.LookupToken(ctx, kt.AuthTokenId, WithUpdateLastAccessedTime(true))
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, beforeP)
|
||||
|
||||
afterP, err := r.LookupToken(ctx, kt.AuthTokenId)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, afterP)
|
||||
assert.Greater(t, afterP.LastAccessedTime, beforeP.LastAccessedTime)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_RemoveStaleTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
u := &user{
|
||||
Id: "user",
|
||||
Address: "address",
|
||||
}
|
||||
at1 := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: u.Id,
|
||||
ExpirationTime: time.Now().Add(1 * time.Minute),
|
||||
}
|
||||
kt1 := KeyringToken{
|
||||
TokenName: "t1",
|
||||
KeyringType: "k1",
|
||||
AuthTokenId: at1.Id,
|
||||
}
|
||||
at2 := &authtokens.AuthToken{
|
||||
Id: "at_2",
|
||||
Token: "at_2_token",
|
||||
UserId: u.Id,
|
||||
ExpirationTime: time.Now().Add(1 * time.Minute),
|
||||
}
|
||||
kt2 := KeyringToken{
|
||||
TokenName: "t2",
|
||||
KeyringType: "k2",
|
||||
AuthTokenId: "at_2",
|
||||
}
|
||||
// this auth token expired a minute ago
|
||||
at3 := &authtokens.AuthToken{
|
||||
Id: "at_3",
|
||||
Token: "at_3_token",
|
||||
UserId: u.Id,
|
||||
ExpirationTime: time.Now().Add(-(1 * time.Minute)),
|
||||
}
|
||||
kt3 := KeyringToken{
|
||||
TokenName: "t3",
|
||||
KeyringType: "k3",
|
||||
AuthTokenId: "at_3",
|
||||
}
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at1, at2, at3}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{kt1.KeyringType, kt1.TokenName}: at1,
|
||||
{kt2.KeyringType, kt2.TokenName}: at2,
|
||||
{kt3.KeyringType, kt3.TokenName}: at3,
|
||||
}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, u.Address, kt1))
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, u.Address, kt2))
|
||||
assert.NoError(t, r.AddKeyringToken(ctx, u.Address, kt3))
|
||||
|
||||
staleTime := time.Now().Add(-(tokenStalenessLimit + 1*time.Hour))
|
||||
freshTime := time.Now().Add(-(tokenStalenessLimit - 1*time.Hour))
|
||||
|
||||
freshAt := &AuthToken{
|
||||
Id: at1.Id,
|
||||
LastAccessedTime: freshTime,
|
||||
}
|
||||
_, err = r.rw.Update(ctx, freshAt, []string{"LastAccessedTime"}, nil)
|
||||
require.NoError(t, err)
|
||||
anotherFreshAt := &AuthToken{
|
||||
Id: at3.Id,
|
||||
LastAccessedTime: freshTime,
|
||||
}
|
||||
_, err = r.rw.Update(ctx, anotherFreshAt, []string{"LastAccessedTime"}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
staleAt := &AuthToken{
|
||||
Id: at2.Id,
|
||||
LastAccessedTime: staleTime,
|
||||
}
|
||||
_, err = r.rw.Update(ctx, staleAt, []string{"LastAccessedTime"}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
lAt, err := r.listTokens(ctx, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, lAt, 3)
|
||||
|
||||
assert.NoError(t, r.cleanExpiredOrOrphanedAuthTokens(ctx))
|
||||
|
||||
lAt, err = r.listTokens(ctx, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, lAt, 1)
|
||||
assert.Equal(t, lAt[0].Id, at1.Id)
|
||||
}
|
||||
|
||||
func TestCleanExpiredOrOrphanedAuthTokens_Errors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
err := cleanExpiredOrOrphanedAuthTokens(ctx, nil, &sync.Map{})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "writer is nil")
|
||||
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
rw := db.New(s)
|
||||
|
||||
err = cleanExpiredOrOrphanedAuthTokens(ctx, rw, &sync.Map{})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "writer isn't part of an inflight transaction")
|
||||
|
||||
rw.DoTx(ctx, 1, db.ExpBackoff{}, func(_ db.Reader, writer db.Writer) error {
|
||||
err := cleanExpiredOrOrphanedAuthTokens(ctx, writer, nil)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "keyringless auth token map is nil")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpsertUserAndAuthToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
rw := db.New(s)
|
||||
|
||||
defaultAt := &authtokens.AuthToken{
|
||||
Id: "at_123",
|
||||
Token: "at_123_token",
|
||||
UserId: "u_123",
|
||||
}
|
||||
|
||||
rw.DoTx(ctx, 1, db.ExpBackoff{}, func(txReader db.Reader, txWriter db.Writer) error {
|
||||
errorCases := []struct {
|
||||
name string
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
addr string
|
||||
at *authtokens.AuthToken
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "nil reader",
|
||||
reader: nil,
|
||||
writer: txWriter,
|
||||
addr: "address",
|
||||
at: defaultAt,
|
||||
errorContains: "reader is nil",
|
||||
},
|
||||
{
|
||||
name: "nil writer",
|
||||
reader: txReader,
|
||||
writer: nil,
|
||||
addr: "address",
|
||||
at: defaultAt,
|
||||
errorContains: "writer is nil",
|
||||
},
|
||||
{
|
||||
name: "writer not in tx",
|
||||
reader: txReader,
|
||||
writer: rw,
|
||||
addr: "address",
|
||||
at: defaultAt,
|
||||
errorContains: "writer isn't part of an inflight transaction",
|
||||
},
|
||||
{
|
||||
name: "empty address",
|
||||
reader: txReader,
|
||||
writer: txWriter,
|
||||
addr: "",
|
||||
at: defaultAt,
|
||||
errorContains: "boundary address is empty",
|
||||
},
|
||||
{
|
||||
name: "auth token is nil",
|
||||
reader: txReader,
|
||||
writer: txWriter,
|
||||
addr: "address",
|
||||
at: nil,
|
||||
errorContains: "auth token is nil",
|
||||
},
|
||||
{
|
||||
name: "auth token missing id",
|
||||
reader: txReader,
|
||||
writer: txWriter,
|
||||
addr: "address",
|
||||
at: &authtokens.AuthToken{
|
||||
Token: "at_123_token",
|
||||
UserId: "u_123",
|
||||
},
|
||||
errorContains: "auth token id is empty",
|
||||
},
|
||||
{
|
||||
name: "auth token missing user id",
|
||||
reader: txReader,
|
||||
writer: txWriter,
|
||||
addr: "address",
|
||||
at: &authtokens.AuthToken{
|
||||
Id: "at_123",
|
||||
Token: "at_123_token",
|
||||
},
|
||||
errorContains: "auth token user id is empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range errorCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := upsertUserAndAuthToken(ctx, tc.reader, tc.writer, tc.addr, tc.at)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, tc.errorContains)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/api/sessions"
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
)
|
||||
|
||||
type SearchableResource string
|
||||
|
||||
const (
|
||||
Unknown SearchableResource = "unknown"
|
||||
Targets SearchableResource = "targets"
|
||||
Sessions SearchableResource = "sessions"
|
||||
)
|
||||
|
||||
func (r SearchableResource) Valid() bool {
|
||||
switch r {
|
||||
case Targets, Sessions:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ToSearchableResource(s string) SearchableResource {
|
||||
switch {
|
||||
case strings.EqualFold(s, string(Targets)):
|
||||
return Targets
|
||||
case strings.EqualFold(s, string(Sessions)):
|
||||
return Sessions
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// SearchParams contains the parameters for searching in the cache
|
||||
type SearchParams struct {
|
||||
// the name of the resource. eg. "targets" or "sessions"
|
||||
Resource SearchableResource
|
||||
// the auth token id for the user id that has resources synced to the cache
|
||||
AuthTokenId string
|
||||
// the optional mql query to use when searching the resources.
|
||||
Query string
|
||||
// the optional bexpr filter string that all results will be filtered by
|
||||
Filter string
|
||||
}
|
||||
|
||||
// SearchResult returns the results from searching the cache.
|
||||
type SearchResult struct {
|
||||
Targets []*targets.Target
|
||||
Sessions []*sessions.Session
|
||||
}
|
||||
|
||||
// SearchService is a domain service that can search across all resources in the
|
||||
// cache.
|
||||
type SearchService struct {
|
||||
searchableResources map[SearchableResource]resourceSearcher
|
||||
}
|
||||
|
||||
// QueryAndLister defines the methods needed for listing an querying the
|
||||
// resources supported by the boundary frontend.
|
||||
type QueryAndLister interface {
|
||||
QueryTargets(context.Context, string, string) ([]*targets.Target, error)
|
||||
ListTargets(context.Context, string) ([]*targets.Target, error)
|
||||
QuerySessions(context.Context, string, string) ([]*sessions.Session, error)
|
||||
ListSessions(context.Context, string) ([]*sessions.Session, error)
|
||||
}
|
||||
|
||||
func NewSearchService(ctx context.Context, repo QueryAndLister) (*SearchService, error) {
|
||||
const op = "cache.NewSearchService"
|
||||
switch {
|
||||
case util.IsNil(repo):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "repo is nil")
|
||||
}
|
||||
return &SearchService{
|
||||
searchableResources: map[SearchableResource]resourceSearcher{
|
||||
Targets: &resourceSearchFns[*targets.Target]{
|
||||
list: repo.ListTargets,
|
||||
query: repo.QueryTargets,
|
||||
searchResult: func(t []*targets.Target) *SearchResult {
|
||||
return &SearchResult{Targets: t}
|
||||
},
|
||||
},
|
||||
Sessions: &resourceSearchFns[*sessions.Session]{
|
||||
list: repo.ListSessions,
|
||||
query: repo.QuerySessions,
|
||||
searchResult: func(s []*sessions.Session) *SearchResult {
|
||||
return &SearchResult{Sessions: s}
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Search returns a SearchResult based on the provided SearchParams. If the
|
||||
// SearchParams doesn't have a valid searchable resource or an auth token id
|
||||
// an error is returned. If the auth token id is unrecognized or is associated
|
||||
// with a user id which doesn't have any resources associated with it an empty
|
||||
// SearchResult is returned. SearchResult will only have at most one field
|
||||
// populated.
|
||||
func (s *SearchService) Search(ctx context.Context, params SearchParams) (*SearchResult, error) {
|
||||
const op = "cache.(SearchService).Search"
|
||||
switch {
|
||||
case !params.Resource.Valid():
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid resource")
|
||||
case params.AuthTokenId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token id")
|
||||
}
|
||||
rSearcher, ok := s.searchableResources[params.Resource]
|
||||
if !ok {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("resource name %q is not recognized", params.Resource))
|
||||
}
|
||||
resp, err := rSearcher.search(ctx, params)
|
||||
if err != nil {
|
||||
err = errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// resourceSearchFns is a struct that collects all the functions needed to
|
||||
// perform a search on a specific resource type.
|
||||
type resourceSearchFns[T any] struct {
|
||||
// list takes a context and an auth token and returns all resources for the
|
||||
// user of that auth token. If the provided auth token is not in the cache
|
||||
// an empty slice and no error is returned.
|
||||
list func(context.Context, string) ([]T, error)
|
||||
// query takes a context, an auth token, and a query string and returns all
|
||||
// resources for that auth token that matches the provided query parameter.
|
||||
// If the provided auth token is not in the cache an empty slice and no
|
||||
// error is returned.
|
||||
query func(context.Context, string, string) ([]T, error)
|
||||
// searchResult is a function which provides a SearchResult based on the
|
||||
// type of T. SearchResult contains different fields for the different
|
||||
// resource types returned, so for example if T is *targets.Target the
|
||||
// returned SearchResult will have it's "Targets" field populated so the
|
||||
// searchResult should take the passed in paramater and assign it to the
|
||||
// appropriate field in the SearchResult.
|
||||
searchResult func([]T) *SearchResult
|
||||
}
|
||||
|
||||
// resourceSearcher is an interface that only resourceSearchFns[T] is expected
|
||||
// to satisfy. Specifying this interface allows the code to have a map with
|
||||
// resourceSearchFns values which have different bound generic types.
|
||||
type resourceSearcher interface {
|
||||
search(ctx context.Context, p SearchParams) (*SearchResult, error)
|
||||
}
|
||||
|
||||
// search will perform a query using the provided query string or a list if the
|
||||
// provided query string is empty and filter than based on the provided filter.
|
||||
// The results are tied to the user id associated with the provided auth token id.
|
||||
// If the auth token id or the associated user are not in the cache no error
|
||||
// is returned and the returned SearchResults will be empty.
|
||||
// search implements searcher.
|
||||
func (l *resourceSearchFns[T]) search(ctx context.Context, p SearchParams) (*SearchResult, error) {
|
||||
const op = "daemon.(resourceSearchFns).search"
|
||||
|
||||
var found []T
|
||||
var err error
|
||||
switch p.Query {
|
||||
case "":
|
||||
found, err = l.list(ctx, p.AuthTokenId)
|
||||
default:
|
||||
found, err = l.query(ctx, p.AuthTokenId, p.Query)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
if p.Filter == "" {
|
||||
return l.searchResult(found), nil
|
||||
}
|
||||
|
||||
e, err := bexpr.CreateEvaluator(p.Filter, bexpr.WithTagName("json"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("couldn't build filter"), errors.WithCode(errors.InvalidParameter))
|
||||
}
|
||||
finalResults := make([]T, 0, len(found))
|
||||
for _, item := range found {
|
||||
if m, err := e.Evaluate(filterItem{item}); err == nil && m {
|
||||
finalResults = append(finalResults, item)
|
||||
}
|
||||
}
|
||||
return l.searchResult(finalResults), nil
|
||||
}
|
||||
|
||||
type filterItem struct {
|
||||
Item any `json:"item"`
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api/sessions"
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewSearchService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("nil repo", func(t *testing.T) {
|
||||
ss, err := NewSearchService(ctx, nil)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "repo is nil")
|
||||
assert.Nil(t, ss)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
r, err := NewRepository(ctx, s, &sync.Map{},
|
||||
mapBasedAuthTokenKeyringLookup(nil),
|
||||
sliceBasedAuthTokenBoundaryReader(nil))
|
||||
assert.NoError(t, err)
|
||||
|
||||
ss, err := NewSearchService(ctx, r)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, ss)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearch_Errors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
r, err := NewRepository(ctx, s, &sync.Map{},
|
||||
mapBasedAuthTokenKeyringLookup(nil),
|
||||
sliceBasedAuthTokenBoundaryReader(nil))
|
||||
assert.NoError(t, err)
|
||||
|
||||
ss, err := NewSearchService(ctx, r)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ss)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
params SearchParams
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "missing resource",
|
||||
params: SearchParams{
|
||||
Resource: "",
|
||||
AuthTokenId: "at_1",
|
||||
},
|
||||
errorContains: "invalid resource",
|
||||
},
|
||||
{
|
||||
name: "missing auth token id",
|
||||
params: SearchParams{
|
||||
Resource: "targets",
|
||||
AuthTokenId: "",
|
||||
},
|
||||
errorContains: "missing auth token id",
|
||||
},
|
||||
{
|
||||
name: "unrecognized resource",
|
||||
params: SearchParams{
|
||||
Resource: "unknown",
|
||||
AuthTokenId: "at_1",
|
||||
},
|
||||
errorContains: "invalid resource",
|
||||
},
|
||||
{
|
||||
name: "bad filter",
|
||||
params: SearchParams{
|
||||
Resource: "targets",
|
||||
AuthTokenId: "at_1",
|
||||
Filter: "unknown=filter?syntax!",
|
||||
},
|
||||
errorContains: "couldn't build filter",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := ss.Search(ctx, tc.params)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, tc.errorContains)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
at := &AuthToken{
|
||||
Id: "at_1",
|
||||
UserId: "u_1",
|
||||
}
|
||||
{
|
||||
u := &user{Id: at.UserId, Address: "address"}
|
||||
rw := db.New(s)
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
require.NoError(t, rw.Create(ctx, at))
|
||||
|
||||
targets := []any{
|
||||
&Target{UserId: u.Id, Id: "t_1", Name: "one", Item: `{"id": "t_1", "name": "one"}`},
|
||||
&Target{UserId: u.Id, Id: "t_2", Name: "two", Item: `{"id": "t_2", "name": "two"}`},
|
||||
}
|
||||
require.NoError(t, rw.CreateItems(ctx, targets))
|
||||
|
||||
sessions := []any{
|
||||
&Session{UserId: u.Id, Id: "s_1", Endpoint: "one", Item: `{"id": "s_1", "endpoint": "one"}`},
|
||||
&Session{UserId: u.Id, Id: "s_2", Endpoint: "two", Item: `{"id": "s_2", "endpoint": "two"}`},
|
||||
}
|
||||
require.NoError(t, rw.CreateItems(ctx, sessions))
|
||||
}
|
||||
|
||||
r, err := NewRepository(ctx, s, &sync.Map{},
|
||||
mapBasedAuthTokenKeyringLookup(nil),
|
||||
sliceBasedAuthTokenBoundaryReader(nil))
|
||||
assert.NoError(t, err)
|
||||
|
||||
ss, err := NewSearchService(ctx, r)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ss)
|
||||
|
||||
t.Run("List targets", func(t *testing.T) {
|
||||
got, err := ss.Search(ctx, SearchParams{
|
||||
Resource: "targets",
|
||||
AuthTokenId: at.Id,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, &SearchResult{Targets: []*targets.Target{
|
||||
{Id: "t_1", Name: "one"},
|
||||
{Id: "t_2", Name: "two"},
|
||||
}}, got)
|
||||
})
|
||||
|
||||
t.Run("query targets", func(t *testing.T) {
|
||||
got, err := ss.Search(ctx, SearchParams{
|
||||
Resource: "targets",
|
||||
AuthTokenId: at.Id,
|
||||
Query: `name="one"`,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, &SearchResult{Targets: []*targets.Target{
|
||||
{Id: "t_1", Name: "one"},
|
||||
}}, got)
|
||||
})
|
||||
|
||||
t.Run("query targets bad column", func(t *testing.T) {
|
||||
got, err := ss.Search(ctx, SearchParams{
|
||||
Resource: "targets",
|
||||
AuthTokenId: at.Id,
|
||||
Query: `item % "one"`,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, `invalid column "item"`)
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
|
||||
t.Run("Filter targets", func(t *testing.T) {
|
||||
got, err := ss.Search(ctx, SearchParams{
|
||||
Resource: "targets",
|
||||
AuthTokenId: at.Id,
|
||||
Filter: `"/item/name" matches "one"`,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, &SearchResult{Targets: []*targets.Target{
|
||||
{Id: "t_1", Name: "one"},
|
||||
}}, got)
|
||||
})
|
||||
|
||||
t.Run("List sessions", func(t *testing.T) {
|
||||
got, err := ss.Search(ctx, SearchParams{
|
||||
Resource: "sessions",
|
||||
AuthTokenId: at.Id,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, &SearchResult{Sessions: []*sessions.Session{
|
||||
{Id: "s_1", Endpoint: "one"},
|
||||
{Id: "s_2", Endpoint: "two"},
|
||||
}}, got)
|
||||
})
|
||||
|
||||
t.Run("query sessions", func(t *testing.T) {
|
||||
got, err := ss.Search(ctx, SearchParams{
|
||||
Resource: "sessions",
|
||||
AuthTokenId: at.Id,
|
||||
Query: `endpoint="one"`,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, &SearchResult{Sessions: []*sessions.Session{
|
||||
{Id: "s_1", Endpoint: "one"},
|
||||
}}, got)
|
||||
})
|
||||
|
||||
t.Run("Filter sessions", func(t *testing.T) {
|
||||
got, err := ss.Search(ctx, SearchParams{
|
||||
Resource: "sessions",
|
||||
AuthTokenId: at.Id,
|
||||
Filter: `"/item/endpoint" matches "one"`,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, &SearchResult{Sessions: []*sessions.Session{
|
||||
{Id: "s_1", Endpoint: "one"},
|
||||
}}, got)
|
||||
})
|
||||
|
||||
t.Run("unrecognized auth token", func(t *testing.T) {
|
||||
got, err := ss.Search(ctx, SearchParams{
|
||||
Resource: "targets",
|
||||
AuthTokenId: "unrecognized",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &SearchResult{Targets: []*targets.Target{}}, got)
|
||||
})
|
||||
}
|
||||
+508
@@ -0,0 +1,508 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
rw := db.New(conn)
|
||||
|
||||
t.Run("missing address", func(t *testing.T) {
|
||||
u := &user{
|
||||
Id: "u1",
|
||||
}
|
||||
assert.ErrorContains(t, rw.Create(ctx, &u), "constraint failed")
|
||||
})
|
||||
|
||||
t.Run("missing id", func(t *testing.T) {
|
||||
u := &user{
|
||||
Address: "address",
|
||||
}
|
||||
assert.ErrorContains(t, rw.Create(ctx, &u), "constraint failed")
|
||||
})
|
||||
|
||||
t.Run("create success", func(t *testing.T) {
|
||||
u := &user{
|
||||
Id: "created",
|
||||
Address: "address",
|
||||
}
|
||||
assert.NoError(t, rw.Create(ctx, &u))
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
u := &user{
|
||||
Id: "update",
|
||||
Address: "address",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, &u))
|
||||
updatedU := u.clone()
|
||||
updatedU.Address = "updated"
|
||||
_, err := rw.Update(ctx, updatedU, []string{"address"}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookedUp := u.clone()
|
||||
require.NoError(t, rw.LookupById(ctx, lookedUp))
|
||||
assert.Equal(t, updatedU.Address, lookedUp.Address)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_NoMoreTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
conn, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
rw := db.New(conn)
|
||||
|
||||
u := &user{
|
||||
Id: "userId",
|
||||
Address: "address",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
tok1 := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_1",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, tok1))
|
||||
tok2 := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_2",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, tok2))
|
||||
assert.NoError(t, rw.LookupById(ctx, u))
|
||||
|
||||
// deleting a single token doesn't remove the user
|
||||
_, err = rw.Exec(ctx, "delete from auth_token where id = ?", []any{tok1.Id})
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, rw.LookupById(ctx, u))
|
||||
|
||||
// deleting both tokens _does_ remove the user
|
||||
_, err = rw.Exec(ctx, "delete from auth_token", nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, errors.IsNotFoundError(rw.LookupById(ctx, u)))
|
||||
}
|
||||
|
||||
func TestAuthToken_NoMoreKeyringTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
rw := db.New(s)
|
||||
|
||||
u := &user{
|
||||
Id: "userId",
|
||||
Address: "address",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
at := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_1234567890",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, at))
|
||||
|
||||
kt1 := &KeyringToken{
|
||||
KeyringType: "k1",
|
||||
TokenName: "n1",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, kt1))
|
||||
kt2 := &KeyringToken{
|
||||
KeyringType: "k2",
|
||||
TokenName: "n2",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, kt2))
|
||||
assert.NoError(t, rw.LookupById(ctx, u))
|
||||
|
||||
// deleting the keyring tokens doesn't remove the user
|
||||
_, err = rw.Exec(ctx, "delete from keyring_token", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, rw.LookupById(ctx, at))
|
||||
assert.NoError(t, rw.LookupById(ctx, u))
|
||||
}
|
||||
|
||||
func TestAuthToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
rw := db.New(s)
|
||||
|
||||
u := &user{
|
||||
Id: "userId",
|
||||
Address: "address",
|
||||
}
|
||||
|
||||
t.Run("no user foreign key constraint", func(t *testing.T) {
|
||||
tok := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_1234567890",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, tok), "constraint failed")
|
||||
})
|
||||
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
t.Run("no user id", func(t *testing.T) {
|
||||
tok := &AuthToken{
|
||||
Id: "at_1234567890",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, tok), "constraint failed")
|
||||
})
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
tok := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_create",
|
||||
}
|
||||
before := time.Now().Truncate(1 * time.Millisecond)
|
||||
require.NoError(t, rw.Create(ctx, tok))
|
||||
require.NoError(t, rw.LookupById(ctx, tok))
|
||||
assert.GreaterOrEqual(t, tok.LastAccessedTime, before)
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
tok := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_update",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, tok))
|
||||
|
||||
tok.LastAccessedTime = time.Now().Add(-(24 * 365 * time.Hour))
|
||||
n, err := rw.Update(ctx, tok, []string{"LastAccessedTime"}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
})
|
||||
|
||||
t.Run("delete user deletes token", func(t *testing.T) {
|
||||
u := &user{
|
||||
Id: "deletethis",
|
||||
Address: "deleted",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
tok := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_deleted",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, tok))
|
||||
|
||||
_, err = rw.Exec(ctx, "delete from user where id = ?", []any{u.Id})
|
||||
|
||||
require.True(t, errors.IsNotFoundError(rw.LookupById(ctx, tok)))
|
||||
})
|
||||
|
||||
// TODO: When gorm sqlite driver fixes it's delete, use rw.Delete instead of the Exec.
|
||||
// n, err := rw.Delete(ctx, p)
|
||||
_, err = rw.Exec(ctx, "delete from auth_token", nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestKeyringToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
rw := db.New(s)
|
||||
|
||||
u := &user{
|
||||
Id: "userId",
|
||||
Address: "address",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
at := &AuthToken{
|
||||
Id: "at_1",
|
||||
UserId: u.Id,
|
||||
}
|
||||
|
||||
t.Run("no token foreign key constraint", func(t *testing.T) {
|
||||
kt := &KeyringToken{
|
||||
KeyringType: "keyring",
|
||||
TokenName: "token",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, kt), "constraint failed")
|
||||
})
|
||||
|
||||
require.NoError(t, rw.Create(ctx, at))
|
||||
|
||||
t.Run("no auth token id", func(t *testing.T) {
|
||||
tok := &KeyringToken{
|
||||
KeyringType: "keyring",
|
||||
TokenName: "token",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, tok), "constraint failed")
|
||||
})
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
kt := &KeyringToken{
|
||||
KeyringType: "keyring",
|
||||
TokenName: "create",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, kt))
|
||||
require.NoError(t, rw.LookupById(ctx, kt))
|
||||
})
|
||||
|
||||
t.Run("delete user deletes keyring token", func(t *testing.T) {
|
||||
u := &user{
|
||||
Id: "deletethis",
|
||||
Address: "deleted",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
at := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_deleted",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, at))
|
||||
|
||||
kt := &KeyringToken{
|
||||
KeyringType: "keyring",
|
||||
TokenName: "deleted",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, kt))
|
||||
require.NoError(t, rw.LookupById(ctx, kt))
|
||||
|
||||
_, err = rw.Exec(ctx, "delete from user where id = ?", []any{u.Id})
|
||||
require.True(t, errors.IsNotFoundError(rw.LookupById(ctx, kt)))
|
||||
})
|
||||
|
||||
t.Run("delete auth token deletes keyring token", func(t *testing.T) {
|
||||
u := &user{
|
||||
Id: "deletethis",
|
||||
Address: "deleted",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
at := &AuthToken{
|
||||
UserId: u.Id,
|
||||
Id: "at_deleted",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, at))
|
||||
|
||||
kt := &KeyringToken{
|
||||
KeyringType: "keyring",
|
||||
TokenName: "deleted",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, kt))
|
||||
require.NoError(t, rw.LookupById(ctx, kt))
|
||||
|
||||
_, err = rw.Exec(ctx, "delete from auth_token where id = ?", []any{at.Id})
|
||||
require.True(t, errors.IsNotFoundError(rw.LookupById(ctx, kt)))
|
||||
})
|
||||
|
||||
// TODO: When gorm sqlite driver fixes it's delete, use rw.Delete instead of the Exec.
|
||||
// n, err := rw.Delete(ctx, p)
|
||||
_, err = rw.Exec(ctx, "delete from keyring_token", nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestTarget(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
rw := db.New(s)
|
||||
|
||||
addr := "boundary"
|
||||
userId := "u_12345"
|
||||
u := &user{
|
||||
Id: userId,
|
||||
Address: addr,
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
t.Run("target without user id", func(t *testing.T) {
|
||||
unknownTarget := &Target{
|
||||
Id: "tssh_1234567890",
|
||||
Name: "target",
|
||||
Description: "target desc",
|
||||
Address: "some address",
|
||||
Item: "{id:'tssh_1234567890'}",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, unknownTarget), "constraint failed")
|
||||
})
|
||||
|
||||
t.Run("target actions", func(t *testing.T) {
|
||||
target := &Target{
|
||||
UserId: u.Id,
|
||||
Id: "tssh_1234567890",
|
||||
Name: "target",
|
||||
Description: "target desc",
|
||||
Address: "some address",
|
||||
Item: "{id:'tssh_1234567890'}",
|
||||
}
|
||||
|
||||
require.NoError(t, rw.Create(ctx, target))
|
||||
|
||||
require.NoError(t, rw.LookupById(ctx, target))
|
||||
|
||||
target.Address = "new address"
|
||||
n, err := rw.Update(ctx, target, []string{"address"}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
|
||||
// TODO: Once the sqlite driver properly builds the delete query call
|
||||
// n, err = rw.Delete(ctx, target) instead of the Exec call
|
||||
n, err = rw.Exec(ctx, "delete from target where (user_id, id) IN (values (?, ?))",
|
||||
[]any{target.UserId, target.Id})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
})
|
||||
|
||||
t.Run("lookup a target", func(t *testing.T) {
|
||||
target := &Target{
|
||||
UserId: u.Id,
|
||||
Id: "tssh_1234567890",
|
||||
Name: "target",
|
||||
Description: "target desc",
|
||||
Address: "some address",
|
||||
Item: "{id:'tssh_1234567890'}",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, target))
|
||||
|
||||
lookTar := &Target{
|
||||
UserId: target.UserId,
|
||||
Id: target.Id,
|
||||
}
|
||||
assert.NoError(t, rw.LookupById(ctx, lookTar))
|
||||
assert.NotNil(t, lookTar)
|
||||
|
||||
// cleanup the targets
|
||||
_, err := rw.Exec(ctx, "delete from target", nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("deleting the user deletes the target", func(t *testing.T) {
|
||||
target := &Target{
|
||||
UserId: u.Id,
|
||||
Id: "tssh_1234567890",
|
||||
Name: "target",
|
||||
Description: "target desc",
|
||||
Address: "some address",
|
||||
Item: "{id:'tssh_1234567890'}",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, target))
|
||||
// Deleting the user deletes the target
|
||||
// TODO: Once the sqlite driver supports proper deletes change from the
|
||||
// Exec call to .Delete
|
||||
// n, err := rw.Delete(ctx, &u)
|
||||
n, err := rw.Exec(ctx, "delete from user where id = ?", []any{userId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, n)
|
||||
|
||||
lookTar := &Target{
|
||||
UserId: target.UserId,
|
||||
Id: target.Id,
|
||||
}
|
||||
assert.ErrorContains(t, rw.LookupById(ctx, lookTar), "not found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
rw := db.New(s)
|
||||
|
||||
addr := "boundary"
|
||||
userId := "u_12345"
|
||||
u := &user{
|
||||
Id: userId,
|
||||
Address: addr,
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, u))
|
||||
|
||||
t.Run("session without user id", func(t *testing.T) {
|
||||
unknownSess := &Session{
|
||||
Id: "sess_1234567890",
|
||||
Item: "{id:'sess_1234567890'}",
|
||||
}
|
||||
require.ErrorContains(t, rw.Create(ctx, unknownSess), "FOREIGN KEY constraint")
|
||||
})
|
||||
t.Run("session actions", func(t *testing.T) {
|
||||
session := &Session{
|
||||
UserId: u.Id,
|
||||
Id: "s_1234567890",
|
||||
Endpoint: "endpoint",
|
||||
Item: "{id:'s_1234567890'}",
|
||||
}
|
||||
|
||||
require.NoError(t, rw.Create(ctx, session))
|
||||
|
||||
require.NoError(t, rw.LookupById(ctx, session))
|
||||
|
||||
session.Endpoint = "new endpoint"
|
||||
n, err := rw.Update(ctx, session, []string{"endpoint"}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
|
||||
// TODO: Once the sqlite driver properly builds the delete query call
|
||||
// n, err = rw.Delete(ctx, session) instead of the Exec call
|
||||
n, err = rw.Exec(ctx, "delete from session where (user_id, id) IN (values (?, ?))",
|
||||
[]any{session.UserId, session.Id})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
})
|
||||
|
||||
t.Run("lookup a session", func(t *testing.T) {
|
||||
session := &Session{
|
||||
UserId: u.Id,
|
||||
Id: "s_1234567890",
|
||||
Endpoint: "endpoint",
|
||||
Item: "{id:'s_1234567890'}",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, session))
|
||||
|
||||
lookSess := &Session{
|
||||
UserId: u.Id,
|
||||
Id: session.Id,
|
||||
}
|
||||
assert.NoError(t, rw.LookupById(ctx, lookSess))
|
||||
assert.NotNil(t, lookSess)
|
||||
|
||||
// cleanup the sessions
|
||||
_, err := rw.Exec(ctx, "delete from session", nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("deleting the user deletes the session", func(t *testing.T) {
|
||||
session := &Session{
|
||||
UserId: u.Id,
|
||||
Id: "s_1234567890",
|
||||
Endpoint: "endpoint",
|
||||
Item: "{id:'s_1234567890'}",
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, session))
|
||||
// Deleting the user deletes the session
|
||||
// TODO: Once the sqlite driver supports proper deletes change from the
|
||||
// Exec call to .Delete
|
||||
// n, err := rw.Delete(ctx, &u)
|
||||
n, err := rw.Exec(ctx, "delete from user where id = ?", []any{userId})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, n)
|
||||
|
||||
lookSess := &Session{
|
||||
UserId: session.UserId,
|
||||
Id: session.Id,
|
||||
}
|
||||
assert.ErrorContains(t, rw.LookupById(ctx, lookSess), "not found")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
/*
|
||||
Package daemon contains the logic for managing the client cache daemon.
|
||||
*/
|
||||
package daemon
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
)
|
||||
|
||||
func writeError(w http.ResponseWriter, msg string, s int) {
|
||||
status := http.StatusText(s)
|
||||
b, err := json.Marshal(&api.Error{
|
||||
Kind: status,
|
||||
Message: msg,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Unable to marshal error {Kind: %s, Message: %q} into api error format: %s", status, msg, err.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Error(w, string(b), s)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
sockAddr = "socket/daemon.sock"
|
||||
|
||||
// Currently we only allow the same user that started the boundary daemon
|
||||
// to connect to the socket to make requests to it.
|
||||
// In linux, sockets visible to the FS honor the perms of the dir they are in.
|
||||
// To create a new socket we must have read/write/list(execute) permissions on
|
||||
// the directory it is being created in.
|
||||
socketDirPerms = 0o700
|
||||
// To connect to a socket it must have read/write permissions
|
||||
socketPerms = 0o600
|
||||
)
|
||||
|
||||
// listener provides a Listener on the daemon unix socket.
|
||||
func listener(ctx context.Context, path string) (net.Listener, error) {
|
||||
const op = "daemon.listener"
|
||||
socketName := filepath.Join(path, sockAddr)
|
||||
if err := os.Remove(socketName); err != nil {
|
||||
// If the socket existed before and wasn't cleaned up delete it now.
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
|
||||
socketPath := filepath.Dir(socketName)
|
||||
if err := os.MkdirAll(socketPath, socketDirPerms); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to create boundary directory"))
|
||||
}
|
||||
|
||||
l, err := net.Listen("unix", socketName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Failed listening"))
|
||||
}
|
||||
if err := os.Chmod(socketName, socketPerms); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("changing socket permissiosn"))
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// SocketAddress returns the unix socket filename with a 'unix://' prefix.
|
||||
// The returned value can be passed directly into the api.Client
|
||||
func SocketAddress(path string) string {
|
||||
return fmt.Sprintf("unix://%s", filepath.Join(path, sockAddr))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO: Write a test for this that can run on windows.
|
||||
func TestListenerSocketPermissions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
path, err := os.MkdirTemp("", "*")
|
||||
require.NoError(t, err)
|
||||
l, err := listener(ctx, path)
|
||||
require.NoError(t, err)
|
||||
socketFile := l.Addr().String()
|
||||
fi, err := os.Stat(socketFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fi.Mode().Type(), os.ModeSocket)
|
||||
assert.Equal(t, fs.FileMode(0o600), fi.Mode().Perm(), "permissions were ", fi.Mode().Perm().String())
|
||||
|
||||
socketDirName := filepath.Dir(socketFile)
|
||||
di, err := os.Stat(socketDirName)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, di.Mode().Type(), os.ModeDir)
|
||||
assert.Equal(t, fs.FileMode(0o700), di.Mode().Perm(), "permissions were ", fi.Mode().Perm().String())
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Note: the name of this test must remain short because the temp dir created
|
||||
// includes the name of the test and there is a 108 character limit in allowed
|
||||
// unix socket path names.
|
||||
func TestListenComms(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
path := t.TempDir()
|
||||
socketListener, err := listener(ctx, path)
|
||||
|
||||
require.NoError(t, err)
|
||||
mux := http.NewServeMux()
|
||||
|
||||
payload := "Hello test"
|
||||
mux.HandleFunc("/v1/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err = fmt.Fprint(w, payload)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
srv := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
assert.ErrorIs(t, srv.Serve(socketListener), http.ErrServerClosed)
|
||||
}()
|
||||
|
||||
client, err := api.NewClient(nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.SetAddr(SocketAddress(path)))
|
||||
client.SetToken("")
|
||||
req, err := client.NewRequest(ctx, "GET", "/test", nil)
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
got, err := io.ReadAll(resp.HttpResponse().Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(got), payload)
|
||||
|
||||
require.NoError(t, srv.Close())
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
withDebug bool
|
||||
withBoundaryTokenReaderFunc cache.BoundaryTokenReaderFn
|
||||
}
|
||||
|
||||
// Option - how options are passed as args
|
||||
type Option func(*options) error
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{}
|
||||
}
|
||||
|
||||
func getOpts(opt ...Option) (options, error) {
|
||||
opts := getDefaultOptions()
|
||||
|
||||
for _, o := range opt {
|
||||
if err := o(&opts); err != nil {
|
||||
return opts, err
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// WithDebug provides an optional debug flag.
|
||||
func WithDebug(_ context.Context, debug bool) Option {
|
||||
return func(o *options) error {
|
||||
o.withDebug = debug
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBoundaryTokenReaderFunc provides an option for specifying a BoundaryTokenReaderFn
|
||||
func WithBoundaryTokenReaderFunc(_ context.Context, fn cache.BoundaryTokenReaderFn) Option {
|
||||
return func(o *options) error {
|
||||
o.withBoundaryTokenReaderFunc = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
opts, err := getOpts()
|
||||
require.NoError(t, err)
|
||||
testOpts := options{}
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithDebug", func(t *testing.T) {
|
||||
opts, err := getOpts(WithDebug(ctx, true))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withDebug = true
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithBoundaryTokenReaderFunc", func(t *testing.T) {
|
||||
var f cache.BoundaryTokenReaderFn = func(ctx context.Context, addr, token string) (*authtokens.AuthToken, error) {
|
||||
return nil, nil
|
||||
}
|
||||
opts, err := getOpts(WithBoundaryTokenReaderFunc(ctx, f))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, opts.withBoundaryTokenReaderFunc)
|
||||
opts.withBoundaryTokenReaderFunc = nil
|
||||
|
||||
testOpts := getDefaultOptions()
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/boundary/api/sessions"
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// SearchResult is the struct returned to search requests.
|
||||
type SearchResult struct {
|
||||
Targets []*targets.Target `json:",omitempty"`
|
||||
Sessions []*sessions.Session `json:",omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
filterKey = "filter"
|
||||
queryKey = "query"
|
||||
resourceKey = "resource"
|
||||
|
||||
boundaryAddrKey = "boundary_addr"
|
||||
authTokenIdKey = "auth_token_id"
|
||||
)
|
||||
|
||||
func newSearchHandlerFunc(ctx context.Context, repo *cache.Repository) (http.HandlerFunc, error) {
|
||||
const op = "daemon.newSearchHandlerFunc"
|
||||
switch {
|
||||
case util.IsNil(repo):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "repository is missing")
|
||||
}
|
||||
|
||||
s, err := cache.NewSearchService(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
resource := r.URL.Query().Get(resourceKey)
|
||||
authTokenId := r.URL.Query().Get(authTokenIdKey)
|
||||
|
||||
searchableResource := cache.ToSearchableResource(resource)
|
||||
switch {
|
||||
case resource == "":
|
||||
writeError(w, "resource is a required field but was empty", http.StatusBadRequest)
|
||||
return
|
||||
case !searchableResource.Valid():
|
||||
writeError(w, "provided resource is not a valid searchable resource", http.StatusBadRequest)
|
||||
return
|
||||
case authTokenId == "":
|
||||
writeError(w, fmt.Sprintf("%s is a required field but was empty", authTokenIdKey), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
t, err := repo.LookupToken(ctx, authTokenId, cache.WithUpdateLastAccessedTime(true))
|
||||
if err != nil || t == nil {
|
||||
writeError(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query().Get(queryKey)
|
||||
filter := r.URL.Query().Get(filterKey)
|
||||
|
||||
res, err := s.Search(ctx, cache.SearchParams{
|
||||
AuthTokenId: authTokenId,
|
||||
Resource: searchableResource,
|
||||
Query: query,
|
||||
Filter: filter,
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Match(errors.T(errors.InvalidParameter), err):
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
default:
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if res == nil {
|
||||
writeError(w, "nil SearchResult generated", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
apiRes := toApiResult(res)
|
||||
j, err := json.Marshal(apiRes)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// toApiResult converts a domain search result to an api search result
|
||||
func toApiResult(sr *cache.SearchResult) *SearchResult {
|
||||
return &SearchResult{
|
||||
Targets: sr.Targets,
|
||||
Sessions: sr.Sessions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base/logging"
|
||||
"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/boundary/version"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
)
|
||||
|
||||
// Commander is an interface that provides a way to get an apiClient
|
||||
// and retrieve the keyring and token information used by a command.
|
||||
type Commander interface {
|
||||
ClientProvider
|
||||
DiscoverKeyringTokenInfo() (string, string, error)
|
||||
ReadTokenFromKeyring(keyringType, tokenName string) *authtokens.AuthToken
|
||||
}
|
||||
|
||||
// ClientProvider is an interface that provides an api.Client
|
||||
type ClientProvider interface {
|
||||
Client(opt ...base.Option) (*api.Client, error)
|
||||
}
|
||||
|
||||
type CacheServer struct {
|
||||
conf *Config
|
||||
|
||||
infoKeys []string
|
||||
info map[string]string
|
||||
|
||||
storeUrl string
|
||||
store *db.DB
|
||||
|
||||
tickerWg *sync.WaitGroup
|
||||
httpSrv *http.Server
|
||||
|
||||
shutdownOnce *sync.Once
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ContextCancel context.CancelFunc
|
||||
RefreshIntervalSeconds int64
|
||||
DatabaseUrl string
|
||||
StoreDebug bool
|
||||
LogLevel string
|
||||
LogFormat string
|
||||
LogWriter io.Writer
|
||||
DotDirectory string
|
||||
}
|
||||
|
||||
func (sc *Config) validate(ctx context.Context) error {
|
||||
const op = "daemon.(serverConfig).validate"
|
||||
switch {
|
||||
case util.IsNil(sc.LogWriter):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing log writter")
|
||||
case util.IsNil(sc.ContextCancel):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing contextCancel")
|
||||
case sc.DotDirectory == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing dot directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// can be called before eventing is setup
|
||||
func New(ctx context.Context, conf *Config) (*CacheServer, error) {
|
||||
const op = "daemon.newServer"
|
||||
if err := conf.validate(ctx); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
s := &CacheServer{
|
||||
conf: conf,
|
||||
info: make(map[string]string),
|
||||
infoKeys: make([]string, 0, 20),
|
||||
tickerWg: new(sync.WaitGroup),
|
||||
shutdownOnce: new(sync.Once),
|
||||
}
|
||||
if err := s.setupLogging(ctx, conf.LogWriter); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *CacheServer) Shutdown(ctx context.Context) error {
|
||||
const op = "daemon.(cacheServer).Shutdown"
|
||||
|
||||
var shutdownErr error
|
||||
s.shutdownOnce.Do(func() {
|
||||
if s.conf.ContextCancel != nil {
|
||||
s.conf.ContextCancel()
|
||||
}
|
||||
srvCtx, srvCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer srvCancel()
|
||||
err := s.httpSrv.Shutdown(srvCtx)
|
||||
if err != nil {
|
||||
shutdownErr = fmt.Errorf("error shutting down server: %w", err)
|
||||
return
|
||||
}
|
||||
s.tickerWg.Wait()
|
||||
event.WriteSysEvent(context.Background(), op, "daemon server shutdown")
|
||||
if err := event.SysEventer().FlushNodes(context.Background()); err != nil {
|
||||
shutdownErr = fmt.Errorf("error flushing eventer nodes: %w", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
return shutdownErr
|
||||
}
|
||||
|
||||
func defaultBoundaryTokenReader(ctx context.Context, cp ClientProvider) (cache.BoundaryTokenReaderFn, error) {
|
||||
const op = "daemon.defaultBoundaryTokenReader"
|
||||
switch {
|
||||
case util.IsNil(cp):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "client provider is nil")
|
||||
}
|
||||
return func(ctx context.Context, addr, tok string) (*authtokens.AuthToken, error) {
|
||||
switch {
|
||||
case addr == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "address is missing")
|
||||
case tok == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token is missing")
|
||||
}
|
||||
atIdParts := strings.SplitN(tok, "_", 4)
|
||||
if len(atIdParts) != 3 {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "auth token is malformed")
|
||||
}
|
||||
atId := strings.Join(atIdParts[:cache.AuthTokenIdSegmentCount], "_")
|
||||
|
||||
c, err := cp.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.SetAddr(addr)
|
||||
c.SetToken(tok)
|
||||
atClient := authtokens.NewClient(c)
|
||||
|
||||
at, err := atClient.Read(ctx, atId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return at.GetItem(), nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Serve will fire up the refresh goroutine and the caching API http server as a
|
||||
// daemon. The daemon bits are included so it's easy for CLI cmds to start the
|
||||
// a cache server
|
||||
func (s *CacheServer) Serve(ctx context.Context, cmd Commander, opt ...Option) error {
|
||||
const op = "daemon.(cacheServer).Serve"
|
||||
switch {
|
||||
case util.IsNil(ctx):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "context is missing")
|
||||
}
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if opts.withBoundaryTokenReaderFunc == nil {
|
||||
opts.withBoundaryTokenReaderFunc, err = defaultBoundaryTokenReader(ctx, cmd)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
|
||||
l, err := listener(ctx, s.conf.DotDirectory)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
s.info["Listening address"] = l.Addr().String()
|
||||
s.infoKeys = append(s.infoKeys, "Listening address")
|
||||
s.info["Store debug"] = strconv.FormatBool(s.conf.StoreDebug)
|
||||
s.infoKeys = append(s.infoKeys, "Store debug")
|
||||
|
||||
if s.store, s.storeUrl, err = openStore(ctx, s.conf.DatabaseUrl, s.conf.StoreDebug); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
s.info["Database URL"] = s.storeUrl
|
||||
s.infoKeys = append(s.infoKeys, "Database URL")
|
||||
|
||||
s.printInfo(ctx)
|
||||
|
||||
repo, err := cache.NewRepository(ctx, s.store, &sync.Map{}, cmd.ReadTokenFromKeyring, opts.withBoundaryTokenReaderFunc)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
// If we have a token info already, add it to the repository immediately so
|
||||
// it can start to get updated.
|
||||
func() {
|
||||
client, err := cmd.Client()
|
||||
if err != nil {
|
||||
event.WriteError(ctx, op, err)
|
||||
return
|
||||
}
|
||||
krType, tokName, err := cmd.DiscoverKeyringTokenInfo()
|
||||
if err != nil {
|
||||
event.WriteError(ctx, op, err)
|
||||
return
|
||||
}
|
||||
if at := cmd.ReadTokenFromKeyring(krType, tokName); at != nil {
|
||||
if err := repo.AddKeyringToken(ctx, client.Addr(), cache.KeyringToken{KeyringType: krType, TokenName: tokName, AuthTokenId: at.Id}); err != nil {
|
||||
event.WriteError(ctx, op, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
refreshService, err := cache.NewRefreshService(ctx, repo)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
tic, err := newRefreshTicker(ctx, s.conf.RefreshIntervalSeconds, refreshService)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
tickingCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
var tickerWg sync.WaitGroup
|
||||
tickerWg.Add(1)
|
||||
go func() {
|
||||
defer tickerWg.Done()
|
||||
tic.start(tickingCtx)
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
searchTargetsFn, err := newSearchHandlerFunc(ctx, repo)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.HandleFunc("/v1/search", searchTargetsFn)
|
||||
|
||||
tokenFn, err := newTokenHandlerFunc(ctx, repo, tic)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.HandleFunc("/v1/tokens", tokenFn)
|
||||
|
||||
stopFn, err := newStopHandlerFunc(ctx, s.conf.ContextCancel)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.Handle("/v1/stop", versionEnforcement(stopFn))
|
||||
|
||||
logger, err := event.SysEventer().StandardLogger(ctx, "daemon.serve: ", event.ErrorType)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
s.httpSrv = &http.Server{
|
||||
Handler: mux,
|
||||
ErrorLog: logger,
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
if err = s.httpSrv.Serve(l); err != nil && err != http.ErrServerClosed && !errors.Is(err, net.ErrClosed) {
|
||||
event.WriteSysEvent(ctx, op, "error closing server", "err", err.Error())
|
||||
}
|
||||
cancel()
|
||||
tickerWg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CacheServer) printInfo(ctx context.Context) {
|
||||
const op = "daemon.(cacheServer).printInfo"
|
||||
verInfo := version.Get()
|
||||
if verInfo.Version != "" {
|
||||
s.infoKeys = append(s.infoKeys, "version")
|
||||
s.info["version"] = verInfo.FullVersionNumber(false)
|
||||
}
|
||||
if verInfo.Revision != "" {
|
||||
s.info["version sha"] = strings.Trim(verInfo.Revision, "'")
|
||||
s.infoKeys = append(s.infoKeys, "version sha")
|
||||
}
|
||||
s.infoKeys = append(s.infoKeys, "cgo")
|
||||
s.info["cgo"] = "disabled"
|
||||
if version.CgoEnabled {
|
||||
s.info["cgo"] = "enabled"
|
||||
}
|
||||
|
||||
// Server configuration output
|
||||
padding := 0
|
||||
for _, k := range s.infoKeys {
|
||||
currPadding := padding - len(k)
|
||||
if currPadding < 2 {
|
||||
padding = len(k) + 2
|
||||
}
|
||||
}
|
||||
sort.Strings(s.infoKeys)
|
||||
|
||||
output := []string{}
|
||||
output = append(output, "==> cache configuration:\n")
|
||||
for _, k := range s.infoKeys {
|
||||
output = append(output, fmt.Sprintf(
|
||||
"%s%s: %s",
|
||||
strings.Repeat(" ", padding-len(k)),
|
||||
strings.Title(k),
|
||||
s.info[k]))
|
||||
}
|
||||
output = append(output, "")
|
||||
|
||||
// Output the header that the server has started
|
||||
output = append(output, "==> cache started! Log data will stream in below:\n")
|
||||
event.WriteSysEvent(ctx, op, strings.Join(output, "\n"))
|
||||
}
|
||||
|
||||
func (s *CacheServer) setupLogging(ctx context.Context, w io.Writer) error {
|
||||
const op = "daemon.(Command).setupLogging"
|
||||
switch {
|
||||
case util.IsNil(w):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "log writer is nil")
|
||||
}
|
||||
|
||||
logFormat := logging.StandardFormat
|
||||
if s.conf.LogFormat != "" {
|
||||
var err error
|
||||
logFormat, err = logging.ParseLogFormat(s.conf.LogFormat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
}
|
||||
|
||||
logLevel := strings.ToLower(strings.TrimSpace(s.conf.LogLevel))
|
||||
if logLevel == "" {
|
||||
logLevel = "info"
|
||||
}
|
||||
|
||||
// Set up logging
|
||||
// Set level based off text value
|
||||
var level hclog.Level
|
||||
switch logLevel {
|
||||
case "trace":
|
||||
level = hclog.Trace
|
||||
case "debug":
|
||||
level = hclog.Debug
|
||||
case "notice", "info":
|
||||
level = hclog.Info
|
||||
case "warn", "warning":
|
||||
level = hclog.Warn
|
||||
case "err", "error":
|
||||
level = hclog.Error
|
||||
default:
|
||||
return fmt.Errorf("%s: unknown log level: %s", op, logLevel)
|
||||
}
|
||||
var logLock sync.Mutex
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Output: w,
|
||||
Level: level,
|
||||
JSONFormat: logFormat == logging.JSONFormat,
|
||||
Mutex: &logLock,
|
||||
})
|
||||
if err := event.InitFallbackLogger(logger); err != nil {
|
||||
return fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
|
||||
s.info["log level"] = level.String()
|
||||
s.infoKeys = append(s.infoKeys, "log level")
|
||||
s.info["log format"] = logFormat.String()
|
||||
s.infoKeys = append(s.infoKeys, "log format")
|
||||
|
||||
var err error
|
||||
if err = setupEventing(ctx, logger, &logLock, logFormat, w); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupEventing(ctx context.Context, logger hclog.Logger, serializationLock *sync.Mutex, logFormat logging.LogFormat, w io.Writer) error {
|
||||
const op = "daemon.setupEventing"
|
||||
switch {
|
||||
case util.IsNil(logger):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "logger is missing")
|
||||
case util.IsNil(serializationLock):
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "serialization lock is missing")
|
||||
}
|
||||
serverName, err := os.Hostname()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: unable to determine hostname: %w", op, err)
|
||||
}
|
||||
serverName = fmt.Sprintf("%s/%s", serverName, "cache")
|
||||
|
||||
var sinkFormat event.SinkFormat
|
||||
switch logFormat {
|
||||
case logging.JSONFormat:
|
||||
sinkFormat = event.JSONSinkFormat
|
||||
default:
|
||||
sinkFormat = event.TextHclogSinkFormat
|
||||
}
|
||||
|
||||
cfg := event.EventerConfig{
|
||||
AuditEnabled: false,
|
||||
ObservationsEnabled: true,
|
||||
SysEventsEnabled: true,
|
||||
Sinks: []*event.SinkConfig{
|
||||
{
|
||||
Name: "default",
|
||||
EventTypes: []event.Type{event.EveryType},
|
||||
Format: sinkFormat,
|
||||
Type: event.WriterSink,
|
||||
WriterConfig: &event.WriterSinkTypeConfig{
|
||||
Writer: w,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := event.InitSysEventer(logger, serializationLock, serverName, event.WithEventerConfig(&cfg)); err != nil {
|
||||
return fmt.Errorf("%s: unable to initialize system eventer: %w", op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func openStore(ctx context.Context, url string, flagDebugStore bool) (*db.DB, string, error) {
|
||||
const op = "daemon.openStore"
|
||||
var err error
|
||||
opts := []cachedb.Option{cachedb.WithDebug(flagDebugStore)}
|
||||
switch {
|
||||
case url != "":
|
||||
url, err = parseutil.ParsePath(url)
|
||||
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
||||
return nil, "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
opts = append(opts, cachedb.WithUrl(url))
|
||||
}
|
||||
store, err := cachedb.Open(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return store, url, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/daemon/controller"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Note: the name of this test must remain short because the temp dir created
|
||||
// includes the name of the test and there is a 108 character limit in allowed
|
||||
// unix socket path names.
|
||||
func TestDefaultBoundaryTokenReader(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("nil client provider", func(t *testing.T) {
|
||||
resFn, err := defaultBoundaryTokenReader(ctx, nil)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "client provider is nil")
|
||||
assert.Nil(t, resFn)
|
||||
})
|
||||
|
||||
tc := controller.NewTestController(t, nil)
|
||||
cp := fakeClientProvider{tc}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
address string
|
||||
token string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
address: tc.ApiAddrs()[0],
|
||||
token: tc.Token().Token,
|
||||
errContains: "",
|
||||
},
|
||||
{
|
||||
name: "empty address",
|
||||
address: "",
|
||||
token: "at_123_testtoken",
|
||||
errContains: "address is missing",
|
||||
},
|
||||
{
|
||||
name: "empty token",
|
||||
address: tc.ApiAddrs()[0],
|
||||
token: "",
|
||||
errContains: "auth token is missing",
|
||||
},
|
||||
{
|
||||
name: "malformed token to many sections",
|
||||
address: tc.ApiAddrs()[0],
|
||||
token: "at_123_ignoredtoken_tomanysections",
|
||||
errContains: "auth token is malformed",
|
||||
},
|
||||
{
|
||||
name: "malformed token to few sections",
|
||||
address: tc.ApiAddrs()[0],
|
||||
token: "at_123",
|
||||
errContains: "auth token is malformed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
retFn, err := defaultBoundaryTokenReader(ctx, cp)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, retFn)
|
||||
|
||||
at, err := retFn(ctx, tc.address, tc.token)
|
||||
switch tc.errContains {
|
||||
case "":
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, at)
|
||||
default:
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, tc.errContains)
|
||||
assert.Nil(t, at)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeClientProvider struct {
|
||||
*controller.TestController
|
||||
}
|
||||
|
||||
func (fcp fakeClientProvider) Client(opt ...base.Option) (*api.Client, error) {
|
||||
return fcp.TestController.Client(), nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// newStopHandlerFunc creates a handler that cancels the server context.
|
||||
// This is only expected to be used on windows currently since it cannot use
|
||||
// POSIX signals to perform a graceful shutdown.
|
||||
func newStopHandlerFunc(ctx context.Context, cancelFn context.CancelFunc) (http.HandlerFunc, error) {
|
||||
const op = "daemon.newSearchTargetsHandlerFunc"
|
||||
switch {
|
||||
case util.IsNil(cancelFn):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "server cancelFn is missing")
|
||||
}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, "only method POST allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
cancelFn()
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStopHandler(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ph, err := newStopHandlerFunc(ctx, cancel)
|
||||
require.NoError(t, err)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/v1/stop", ph)
|
||||
|
||||
t.Run("get", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/stop", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, rec.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/v1/stop", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, rec.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/stop", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusNoContent, rec.Result().StatusCode)
|
||||
assert.Error(t, ctx.Err())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"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/cache"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type TestServer struct {
|
||||
*CacheServer
|
||||
dotDir string
|
||||
cmd Commander
|
||||
}
|
||||
|
||||
// NewTestServer creates a test cache server using reasonable defaults for
|
||||
// tests. Supports the option WithDebugFlag to enable debug output for sql
|
||||
func NewTestServer(t *testing.T, cmd Commander, opt ...Option) *TestServer {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
require.NoError(t, err)
|
||||
dotDir := t.TempDir()
|
||||
|
||||
cfg := &Config{
|
||||
ContextCancel: cancel,
|
||||
RefreshIntervalSeconds: DefaultRefreshIntervalSeconds,
|
||||
StoreDebug: opts.withDebug,
|
||||
LogWriter: io.Discard,
|
||||
DotDirectory: dotDir,
|
||||
}
|
||||
|
||||
s, err := New(ctx, cfg)
|
||||
require.NoError(t, err)
|
||||
return &TestServer{
|
||||
CacheServer: s,
|
||||
dotDir: dotDir,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// BaseDotDir returns the base directory in which the daemon socket is
|
||||
// created.
|
||||
func (s *TestServer) BaseDotDir() string {
|
||||
return s.dotDir
|
||||
}
|
||||
|
||||
// Serve runs the cache server. This is a blocking call and returns when the
|
||||
// server is shutdown or stops for any other reason.
|
||||
func (s *TestServer) Serve(t *testing.T, opt ...Option) error {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
t.Cleanup(func() {
|
||||
s.Shutdown(ctx)
|
||||
})
|
||||
return s.CacheServer.Serve(ctx, s.cmd, opt...)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
r, err := cache.NewRepository(ctx, s.CacheServer.store, &sync.Map{}, s.cmd.ReadTokenFromKeyring, atReadFn)
|
||||
require.NoError(t, err)
|
||||
|
||||
tarFn := func(ctx context.Context, _, tok string) ([]*targets.Target, error) {
|
||||
if tok != p.Token {
|
||||
return nil, nil
|
||||
}
|
||||
return tars, nil
|
||||
}
|
||||
sessFn := func(ctx context.Context, _, tok string) ([]*sessions.Session, error) {
|
||||
if tok != p.Token {
|
||||
return nil, nil
|
||||
}
|
||||
return sess, nil
|
||||
}
|
||||
rs, err := cache.NewRefreshService(ctx, r)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, rs.Refresh(ctx, cache.WithTargetRetrievalFunc(tarFn), cache.WithSessionRetrievalFunc(sessFn)))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultRefreshIntervalSeconds = 5 * 60
|
||||
defaultRefreshInterval = DefaultRefreshIntervalSeconds * time.Second
|
||||
)
|
||||
|
||||
type refreshService interface {
|
||||
Refresh(context.Context, ...cache.Option) error
|
||||
}
|
||||
|
||||
type refreshTicker struct {
|
||||
tickerCtx context.Context
|
||||
refreshInterval time.Duration
|
||||
refreshChan chan struct{}
|
||||
refresher refreshService
|
||||
}
|
||||
|
||||
func newRefreshTicker(ctx context.Context, refreshIntervalSeconds int64, refresh refreshService) (*refreshTicker, error) {
|
||||
const op = "daemon.newRefreshTicker"
|
||||
switch {
|
||||
case refreshIntervalSeconds == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "refresh interval seconds is missing")
|
||||
case util.IsNil(refresh):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "refreshing function is missing")
|
||||
}
|
||||
|
||||
refreshInterval := defaultRefreshInterval
|
||||
if refreshIntervalSeconds != 0 {
|
||||
refreshInterval = time.Duration(refreshIntervalSeconds) * time.Second
|
||||
}
|
||||
return &refreshTicker{
|
||||
refreshInterval: refreshInterval,
|
||||
refresher: refresh,
|
||||
|
||||
// We make this channel size 1 so if something happens midway through the refresh
|
||||
// we can immediately refresh again immediately to pick up something that might have been
|
||||
// missed the first time through.
|
||||
refreshChan: make(chan struct{}, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// refresh starts the refresh logic if it is currently waiting. If refresh is
|
||||
// currently happening then this queues a refresh for when the current refresh
|
||||
// iteration is done, unless there is already a refresh queued in which case
|
||||
// this is a noop.
|
||||
func (rt *refreshTicker) refresh() {
|
||||
select {
|
||||
case rt.refreshChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *refreshTicker) start(ctx context.Context) {
|
||||
const op = "daemon.(refreshTicker).start"
|
||||
timer := time.NewTimer(0)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
case <-rt.refreshChan:
|
||||
}
|
||||
if err := rt.refresher.Refresh(ctx); err != nil {
|
||||
event.WriteError(rt.tickerCtx, op, err)
|
||||
}
|
||||
timer.Reset(rt.refreshInterval)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTickerRefresh(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
refresher := &fakeRefresher{make(chan struct{})}
|
||||
|
||||
rt, err := newRefreshTicker(ctx, 60, refresher)
|
||||
require.NoError(t, err)
|
||||
|
||||
tickerCtx, tickerCancel := context.WithCancel(ctx)
|
||||
go rt.start(tickerCtx)
|
||||
t.Cleanup(func() {
|
||||
tickerCancel()
|
||||
})
|
||||
|
||||
// let the normal start ticker refresh things,
|
||||
<-refresher.called
|
||||
|
||||
testCtx, testCancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer testCancel()
|
||||
rt.refresh()
|
||||
select {
|
||||
case <-refresher.called:
|
||||
case <-testCtx.Done():
|
||||
assert.Fail(t, "timed out waiting for the refresh ")
|
||||
}
|
||||
|
||||
// wait and make sure we don't get yet another call
|
||||
select {
|
||||
case <-refresher.called:
|
||||
assert.Fail(t, "received an unexpected refresh call")
|
||||
case <-testCtx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRefresher struct {
|
||||
called chan struct{}
|
||||
}
|
||||
|
||||
func (r *fakeRefresher) Refresh(context.Context, ...cache.Option) error {
|
||||
r.called <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
type refresher interface {
|
||||
refresh()
|
||||
}
|
||||
|
||||
// KeyringToken has keyring held auth token information.
|
||||
type KeyringToken struct {
|
||||
// The keyring type used by boundary to access the auth token
|
||||
KeyringType string
|
||||
// The token identifier for the provided keyring type that holds the auth token
|
||||
TokenName string
|
||||
}
|
||||
|
||||
// userTokenToAdd is the request body to this handler.
|
||||
type UpsertTokenRequest struct {
|
||||
// BoundaryAddr is a required field for all requests
|
||||
BoundaryAddr string
|
||||
// The id of the auth token asserted to be attempted to be added
|
||||
AuthTokenId string
|
||||
// The raw auth token for this user. Either this field or the Keyring field
|
||||
// must be set but not both.
|
||||
AuthToken string
|
||||
// Keyring is the keyring info used when adding an auth token held in
|
||||
// keyring to the daemon.
|
||||
Keyring *KeyringToken
|
||||
}
|
||||
|
||||
func newTokenHandlerFunc(ctx context.Context, repo *cache.Repository, refresher refresher) (http.HandlerFunc, error) {
|
||||
const op = "daemon.newPersonaHandlerFunc"
|
||||
switch {
|
||||
case util.IsNil(repo):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "repository is nil")
|
||||
case util.IsNil(refresher):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "refresher is nil")
|
||||
}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var perReq UpsertTokenRequest
|
||||
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeError(w, "unable to read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(data, &perReq); err != nil {
|
||||
writeError(w, "unable to parse request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case perReq.BoundaryAddr == "":
|
||||
writeError(w, "BoundaryAddr is a required field but was empty", http.StatusBadRequest)
|
||||
return
|
||||
case perReq.AuthTokenId == "":
|
||||
writeError(w, "AuthTokenId is a required field but was empty", http.StatusBadRequest)
|
||||
return
|
||||
case perReq.Keyring == nil && perReq.AuthToken == "":
|
||||
writeError(w, "Either keyring info or the authtoken must be provided but were empty", http.StatusBadRequest)
|
||||
return
|
||||
case perReq.Keyring != nil:
|
||||
switch {
|
||||
case perReq.Keyring.TokenName == "":
|
||||
writeError(w, "TokenName is a required field but was empty", http.StatusBadRequest)
|
||||
return
|
||||
case perReq.Keyring.KeyringType == "":
|
||||
writeError(w, "KeyringType is a required field but was empty", http.StatusBadRequest)
|
||||
return
|
||||
case perReq.Keyring.KeyringType == base.NoneKeyring:
|
||||
writeError(w, fmt.Sprintf("KeyringType is set to %s which is not supported", perReq.Keyring.KeyringType), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
case perReq.AuthToken != "":
|
||||
switch {
|
||||
case !strings.HasPrefix(perReq.AuthToken, perReq.AuthTokenId):
|
||||
writeError(w, "The auth token id doesn't match the auth token's prefix", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tok, err := repo.LookupToken(ctx, perReq.AuthTokenId)
|
||||
if err != nil {
|
||||
writeError(w, "error performing auth token lookup", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case perReq.Keyring != nil:
|
||||
kt := cache.KeyringToken{
|
||||
KeyringType: perReq.Keyring.KeyringType,
|
||||
TokenName: perReq.Keyring.TokenName,
|
||||
AuthTokenId: perReq.AuthTokenId,
|
||||
}
|
||||
if err = repo.AddKeyringToken(ctx, perReq.BoundaryAddr, kt); err != nil {
|
||||
writeError(w, "Failed to add a keyring stored token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
case perReq.AuthToken != "":
|
||||
if err = repo.AddRawToken(ctx, perReq.BoundaryAddr, perReq.AuthToken); err != nil {
|
||||
writeError(w, "Failed to add a raw token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
if tok == nil {
|
||||
refresher.refresh()
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ringToken is a test struct used to group a keyring type and token name
|
||||
// so it can be used in an authtoken lookup function.
|
||||
type ringToken struct {
|
||||
k string
|
||||
t string
|
||||
}
|
||||
|
||||
// mapBasedAuthTokenKeyringLookup provides a fake KeyringTokenLookupFn that uses
|
||||
// the provided map to perform lookups for the tokens
|
||||
func mapBasedAuthTokenKeyringLookup(m map[ringToken]*authtokens.AuthToken) cache.KeyringTokenLookupFn {
|
||||
return func(k, t string) *authtokens.AuthToken {
|
||||
return m[ringToken{k, t}]
|
||||
}
|
||||
}
|
||||
|
||||
// sliceBasedAuthTokenBoundaryReader provides a fake BoundaryTokenReaderFn that uses
|
||||
// the provided map to lookup an auth tokens information.
|
||||
func sliceBasedAuthTokenBoundaryReader(s []*authtokens.AuthToken) cache.BoundaryTokenReaderFn {
|
||||
return func(ctx context.Context, addr, at string) (*authtokens.AuthToken, error) {
|
||||
for _, v := range s {
|
||||
if at == v.Token {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
}
|
||||
|
||||
type testRefresher struct {
|
||||
called bool
|
||||
}
|
||||
|
||||
func (r *testRefresher) refresh() {
|
||||
r.called = true
|
||||
}
|
||||
|
||||
func addToken(t *testing.T, h http.Handler, tokReq *UpsertTokenRequest) *api.Error {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(tokReq)
|
||||
require.NoError(t, err)
|
||||
rawBody := bytes.NewBuffer(b)
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/tokens", rawBody)
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Result().StatusCode == http.StatusNoContent {
|
||||
return nil
|
||||
}
|
||||
apiErr := &api.Error{}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), apiErr))
|
||||
return apiErr
|
||||
}
|
||||
|
||||
func TestKeyringToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: "user",
|
||||
}
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
||||
keyring := "k"
|
||||
tokenName := "t"
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{keyring, tokenName}: at,
|
||||
}
|
||||
r, err := cache.NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
tr := &testRefresher{}
|
||||
ph, err := newTokenHandlerFunc(ctx, r, tr)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("missing keyring", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
Keyring: &KeyringToken{
|
||||
KeyringType: "",
|
||||
TokenName: tokenName,
|
||||
},
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
require.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "KeyringType is a required field but was empty")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("none keyring", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
Keyring: &KeyringToken{
|
||||
KeyringType: base.NoneKeyring,
|
||||
TokenName: tokenName,
|
||||
},
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
require.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "KeyringType is set to none which is not supported")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("missing token name", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
Keyring: &KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: "",
|
||||
},
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "TokenName is a required field but was empty")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("missing boundary address", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
Keyring: &KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: tokenName,
|
||||
},
|
||||
BoundaryAddr: "",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "BoundaryAddr is a required field but was empty")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("missing auth token id", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
Keyring: &KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: tokenName,
|
||||
},
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: "",
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "AuthTokenId is a required field but was empty")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("mismatched auth token id", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
Keyring: &KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: tokenName,
|
||||
},
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: "at_doesntmatch",
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "Failed to add a keyring stored token")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
Keyring: &KeyringToken{
|
||||
KeyringType: keyring,
|
||||
TokenName: tokenName,
|
||||
},
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: at.Id,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.True(t, tr.called)
|
||||
|
||||
p, err := r.LookupToken(ctx, pa.AuthTokenId)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, p)
|
||||
assert.Equal(t, at.Id, p.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyringlessToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
at := &authtokens.AuthToken{
|
||||
Id: "at_1",
|
||||
Token: "at_1_token",
|
||||
UserId: "user",
|
||||
}
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{}
|
||||
r, err := cache.NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
tr := &testRefresher{}
|
||||
ph, err := newTokenHandlerFunc(ctx, r, tr)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("missing boundary address", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
BoundaryAddr: "",
|
||||
AuthTokenId: at.Id,
|
||||
AuthToken: at.Token,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "BoundaryAddr is a required field but was empty")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("missing auth token id", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: "",
|
||||
AuthToken: at.Token,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "AuthTokenId is a required field but was empty")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("mismatched auth token id", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: "at_doesntmatch",
|
||||
AuthToken: at.Token,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.NotNil(t, apiErr)
|
||||
assert.Contains(t, apiErr.Message, "The auth token id doesn't match the auth token's prefix")
|
||||
assert.False(t, tr.called)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
pa := &UpsertTokenRequest{
|
||||
BoundaryAddr: "http://127.0.0.1",
|
||||
AuthTokenId: at.Id,
|
||||
AuthToken: at.Token,
|
||||
}
|
||||
apiErr := addToken(t, ph, pa)
|
||||
assert.Nil(t, apiErr)
|
||||
assert.True(t, tr.called)
|
||||
|
||||
p, err := r.LookupToken(ctx, pa.AuthTokenId)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, p)
|
||||
assert.Equal(t, at.Id, p.Id)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/boundary/version"
|
||||
)
|
||||
|
||||
const (
|
||||
VersionHeaderKey = "boundary_version"
|
||||
)
|
||||
|
||||
// versionEnforcement is an interceptor which, if the boundary version is included
|
||||
// in a request, enforces that it matches the version of the daemon currently
|
||||
// running. If no version is provided, the inteceptor passes the request through.
|
||||
func versionEnforcement(h http.Handler) http.Handler {
|
||||
if util.IsNil(h) {
|
||||
return nil
|
||||
}
|
||||
|
||||
needVer := version.Get().VersionNumber()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotVer := r.Header.Get(VersionHeaderKey)
|
||||
if gotVer != "" && needVer != gotVer {
|
||||
writeError(w, fmt.Sprintf("Version mismatch between requester (%q) and daemon (%q). You may need to restart your daemon.", gotVer, needVer), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVersionEnforcement(t *testing.T) {
|
||||
var called bool
|
||||
calledHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
h := versionEnforcement(calledHandler)
|
||||
|
||||
t.Run("no version provided", func(t *testing.T) {
|
||||
called = false
|
||||
req := httptest.NewRequest("", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, http.StatusNoContent, w.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("bad version provided", func(t *testing.T) {
|
||||
called = false
|
||||
req := httptest.NewRequest("", "/test", nil)
|
||||
req.Header.Set(VersionHeaderKey, "badversion")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
assert.False(t, called)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("correct version provided", func(t *testing.T) {
|
||||
called = false
|
||||
req := httptest.NewRequest("", "/test", nil)
|
||||
req.Header.Set(VersionHeaderKey, version.Get().VersionNumber())
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, http.StatusNoContent, w.Result().StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
var cacheSchema string
|
||||
|
||||
// DefaultStoreUrl uses a temp in-memory sqlite database see: https://www.sqlite.org/inmemorydb.html
|
||||
const DefaultStoreUrl = "file::memory:?_pragma=foreign_keys(1)"
|
||||
|
||||
// Open creates a database connection. WithUrl is supported, but by default it
|
||||
// uses an in memory sqlite table. Sqlite is the only supported dbtype.
|
||||
func Open(ctx context.Context, opt ...Option) (*db.DB, error) {
|
||||
const op = "db.Open"
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
var url string
|
||||
switch {
|
||||
case opts.withUrl != "":
|
||||
url = opts.withUrl
|
||||
default:
|
||||
url = DefaultStoreUrl
|
||||
}
|
||||
conn, err := db.Open(ctx, db.Sqlite, url)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
conn.Debug(opts.withDebug)
|
||||
|
||||
switch {
|
||||
case opts.withDbType == dbw.Sqlite:
|
||||
if err := createTables(ctx, conn); err != nil {
|
||||
errors.Wrap(ctx, err, op)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%q is not a supported cache store type", opts.withDbType))
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func createTables(ctx context.Context, conn *db.DB) error {
|
||||
const op = "db.createTables"
|
||||
rw := db.New(conn)
|
||||
if _, err := rw.Exec(ctx, cacheSchema, nil); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
withDebug bool
|
||||
withUrl string
|
||||
withDbType dbw.DbType
|
||||
}
|
||||
|
||||
// Option - how options are passed as args
|
||||
type Option func(*options) error
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{
|
||||
withDbType: dbw.Sqlite,
|
||||
}
|
||||
}
|
||||
|
||||
func getOpts(opt ...Option) (options, error) {
|
||||
opts := getDefaultOptions()
|
||||
|
||||
for _, o := range opt {
|
||||
if err := o(&opts); err != nil {
|
||||
return opts, err
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// WithUrls provides optional url
|
||||
func WithUrl(url string) Option {
|
||||
return func(o *options) error {
|
||||
o.withUrl = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDebug provides an optional debug flag.
|
||||
func WithDebug(debug bool) Option {
|
||||
return func(o *options) error {
|
||||
o.withDebug = debug
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-dbw"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
opts, err := getOpts()
|
||||
require.NoError(t, err)
|
||||
testOpts := options{
|
||||
withDbType: dbw.Sqlite,
|
||||
}
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithUrl", func(t *testing.T) {
|
||||
url := "something"
|
||||
opts, err := getOpts(WithUrl(url))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withUrl = url
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
t.Run("WithDebug", func(t *testing.T) {
|
||||
opts, err := getOpts(WithDebug(true))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withDebug = true
|
||||
assert.Equal(t, opts, testOpts)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
-- Copyright (c) HashiCorp, Inc.
|
||||
-- SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
begin;
|
||||
-- user contains the boundary user information for the boundary user that owns
|
||||
-- the information in the cache.
|
||||
create table if not exists user (
|
||||
-- The id of the user resource from boundary
|
||||
id text not null primary key
|
||||
check (length(id) > 0),
|
||||
-- The address of the boundary instance that this user id comes from
|
||||
address text not null
|
||||
check (length(address) > 0)
|
||||
);
|
||||
|
||||
-- Contains the boundary auth token
|
||||
create table if not exists auth_token (
|
||||
-- id is the boundary id of the auth token
|
||||
id text not null primary key
|
||||
check (length(id) > 0),
|
||||
-- user id is the boundary user id the auth token is associated with
|
||||
user_id text not null
|
||||
references user(id)
|
||||
on delete cascade,
|
||||
-- the last time this the auth token was used on this machine to access
|
||||
-- boundary outside of the context of the cache.
|
||||
last_accessed_time timestamp not null
|
||||
default (strftime('%Y-%m-%d %H:%M:%f','now')),
|
||||
expiration_time timestamp not null
|
||||
);
|
||||
|
||||
-- *delete_orphaned_users triggers delete a user when it no longer has any
|
||||
-- auth tokens associated with them
|
||||
create trigger token_update_delete_orphaned_users after update on auth_token
|
||||
begin
|
||||
delete from user
|
||||
where
|
||||
id not in (select user_id from auth_token);
|
||||
end;
|
||||
|
||||
create trigger token_delete_delete_orphaned_users after delete on auth_token
|
||||
begin
|
||||
delete from user
|
||||
where
|
||||
id not in (select user_id from auth_token);
|
||||
end;
|
||||
|
||||
create table if not exists keyring_token (
|
||||
-- the name of the keyring type on the local machine
|
||||
keyring_type text not null
|
||||
check (length(keyring_type) > 0),
|
||||
-- the name of the stored token on the keyring
|
||||
token_name text not null
|
||||
check (length(token_name) > 0),
|
||||
-- the boundary auth token id stored at in this keyring using the token name
|
||||
auth_token_id text not null
|
||||
references auth_token(id)
|
||||
on delete cascade,
|
||||
primary key (keyring_type, token_name)
|
||||
);
|
||||
|
||||
-- target contains cached boundary target resource for a specific user and with
|
||||
-- specific fields extracted to facilitate searching over those fields
|
||||
create table if not exists target (
|
||||
-- the boundary user id of the user who has was able to read/list this target
|
||||
user_id text not null
|
||||
references user(id)
|
||||
on delete cascade,
|
||||
-- the boundary id of the target
|
||||
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
|
||||
name text,
|
||||
description text,
|
||||
address text,
|
||||
-- item is the json representation of this resource from the perspective of
|
||||
-- the the requesting user.
|
||||
item text,
|
||||
primary key (user_id, id)
|
||||
);
|
||||
|
||||
-- session contains cached boundary session resource for a specific user and
|
||||
-- with specific fields extracted to facilitate searching over those fields
|
||||
create table if not exists session (
|
||||
-- the boundary user id of the user who has was able to read/list this resource
|
||||
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
|
||||
endpoint text,
|
||||
type text,
|
||||
status text,
|
||||
-- item is the json representation of this resource from the perspective of
|
||||
-- of the user whose id is set in user_id
|
||||
item text,
|
||||
primary key (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 (
|
||||
user_id text not null
|
||||
references user(id)
|
||||
on delete cascade,
|
||||
resource_type text not null,
|
||||
error text not null,
|
||||
create_time timestamp not null default current_timestamp,
|
||||
primary key (user_id, resource_type)
|
||||
);
|
||||
|
||||
commit;
|
||||
@@ -102,7 +102,7 @@ type Command struct {
|
||||
FlagTokenName string
|
||||
FlagKeyringType string
|
||||
FlagRecoveryConfig string
|
||||
flagOutputCurlString bool
|
||||
FlagOutputCurlString bool
|
||||
|
||||
FlagScopeId string
|
||||
FlagScopeName string
|
||||
@@ -204,8 +204,8 @@ func (c *Command) Client(opt ...Option) (*api.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.flagOutputCurlString {
|
||||
config.OutputCurlString = c.flagOutputCurlString
|
||||
if c.FlagOutputCurlString {
|
||||
config.OutputCurlString = c.FlagOutputCurlString
|
||||
}
|
||||
|
||||
c.client, err = api.NewClient(config)
|
||||
@@ -452,7 +452,7 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets {
|
||||
|
||||
f.BoolVar(&BoolVar{
|
||||
Name: "output-curl-string",
|
||||
Target: &c.flagOutputCurlString,
|
||||
Target: &c.FlagOutputCurlString,
|
||||
Usage: "Instead of executing the request, print an equivalent cURL command string and exit.",
|
||||
})
|
||||
}
|
||||
|
||||
+764
-898
File diff suppressed because it is too large.
Load diff
@@ -89,7 +89,7 @@ func saveAndOrPrintToken(c *base.Command, result *authmethods.AuthenticateResult
|
||||
}
|
||||
|
||||
if !gotErr {
|
||||
c.UI.Output("\nThe token was successfully stored in the chosen keyring and is not displayed here.")
|
||||
c.UI.Output(fmt.Sprintf("\nThe token name %q was successfully stored in the chosen keyring and is not displayed here.", tokenName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package unsupported
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
var _ cli.Command = (*UnsupportedCommand)(nil)
|
||||
|
||||
// UnsupportedCommand is a command that simply prints out a message indicating
|
||||
// the requested command is not supported on this platform.
|
||||
type UnsupportedCommand struct {
|
||||
*base.Command
|
||||
CommandName string
|
||||
}
|
||||
|
||||
func (c *UnsupportedCommand) notice() string {
|
||||
return fmt.Sprintf("'boundary %s' is not supported on this platform.", c.CommandName)
|
||||
}
|
||||
|
||||
func (c *UnsupportedCommand) Synopsis() string {
|
||||
return c.notice()
|
||||
}
|
||||
|
||||
func (c *UnsupportedCommand) Help() string {
|
||||
return c.notice()
|
||||
}
|
||||
|
||||
func (c *UnsupportedCommand) Run(args []string) int {
|
||||
c.Command.PrintCliError(errors.New(c.notice()))
|
||||
return base.CommandUserError
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package cmd
|
||||
|
||||
// MakeSigUSR2Ch does nothing useful on Windows.
|
||||
|
||||
@@ -144,7 +144,7 @@ func Run(args []string) int {
|
||||
|
||||
// RunCustom allows passing in a base command template to pass to other
|
||||
// commands. Currently, this is only used for setting a custom token helper.
|
||||
func RunCustom(args []string, runOpts *RunOptions) int {
|
||||
func RunCustom(args []string, runOpts *RunOptions) (exitCode int) {
|
||||
if runOpts == nil {
|
||||
runOpts = &RunOptions{}
|
||||
}
|
||||
@@ -256,7 +256,8 @@ func RunCustom(args []string, runOpts *RunOptions) int {
|
||||
AutocompleteNoDefaultFlags: true,
|
||||
}
|
||||
|
||||
exitCode, err := cli.Run()
|
||||
var err error
|
||||
exitCode, err = cli.Run()
|
||||
if outputCurlString {
|
||||
if exitCode == 0 {
|
||||
fmt.Fprint(runOpts.Stderr, "Could not generate cURL command\n")
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/clientcache/cmd/daemon"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/cmd/search"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commandFactoryWrapper = daemonWrap
|
||||
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
delete(Commands, "daemon")
|
||||
Commands["daemon start"] = func() (cli.Command, error) {
|
||||
return &daemon.StartCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon stop"] = func() (cli.Command, error) {
|
||||
return &daemon.StopCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon add-token"] = func() (cli.Command, error) {
|
||||
return &daemon.AddTokenCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["search"] = func() (cli.Command, error) {
|
||||
return &search.SearchCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// daemonWrap wraps the provided wrappableCommand with a daemon writer which
|
||||
// conditionally starts the client side daemon after the command completes
|
||||
func daemonWrap(ui cli.Ui, c wrappableCommand) cli.CommandFactory {
|
||||
return daemon.Wrap(ui, c)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/clientcache/cmd/daemon"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/cmd/search"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commandFactoryWrapper = daemonWrap
|
||||
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
delete(Commands, "daemon")
|
||||
Commands["daemon start"] = func() (cli.Command, error) {
|
||||
return &daemon.StartCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon stop"] = func() (cli.Command, error) {
|
||||
return &daemon.StopCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon add-token"] = func() (cli.Command, error) {
|
||||
return &daemon.AddTokenCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["search"] = func() (cli.Command, error) {
|
||||
return &search.SearchCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// daemonWrap wraps the provided wrappableCommand with a daemon writer which
|
||||
// conditionally starts the client side daemon after the command completes
|
||||
func daemonWrap(ui cli.Ui, c wrappableCommand) cli.CommandFactory {
|
||||
return daemon.Wrap(ui, c)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/clientcache/cmd/daemon"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/cmd/search"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commandFactoryWrapper = daemonWrap
|
||||
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
delete(Commands, "daemon")
|
||||
Commands["daemon start"] = func() (cli.Command, error) {
|
||||
return &daemon.StartCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon stop"] = func() (cli.Command, error) {
|
||||
return &daemon.StopCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon add-token"] = func() (cli.Command, error) {
|
||||
return &daemon.AddTokenCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["search"] = func() (cli.Command, error) {
|
||||
return &search.SearchCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// daemonWrap wraps the provided wrappableCommand with a daemon writer which
|
||||
// conditionally starts the client side daemon after the command completes
|
||||
func daemonWrap(ui cli.Ui, c wrappableCommand) cli.CommandFactory {
|
||||
return daemon.Wrap(ui, c)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/clientcache/cmd/daemon"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/cmd/search"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commandFactoryWrapper = daemonWrap
|
||||
|
||||
extraCommandsFuncs = append(extraCommandsFuncs, func(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
delete(Commands, "daemon")
|
||||
Commands["daemon start"] = func() (cli.Command, error) {
|
||||
return &daemon.StartCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon stop"] = func() (cli.Command, error) {
|
||||
return &daemon.StopCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon add-token"] = func() (cli.Command, error) {
|
||||
return &daemon.AddTokenCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["search"] = func() (cli.Command, error) {
|
||||
return &search.SearchCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// daemonWrap wraps the provided wrappableCommand with a daemon writer which
|
||||
// conditionally starts the client side daemon after the command completes
|
||||
func daemonWrap(ui cli.Ui, c wrappableCommand) cli.CommandFactory {
|
||||
return daemon.Wrap(ui, c)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
_ "github.com/jackc/pgx/v5"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
)
|
||||
|
||||
@@ -22,17 +23,25 @@ func init() {
|
||||
dbw.InitNonUpdatableFields([]string{"PublicId", "CreateTime", "UpdateTime"})
|
||||
}
|
||||
|
||||
// sqliteOpen is a function used to get the dbw.Dialector for sqlite. It returns
|
||||
// an error if there is any problem opening it.
|
||||
var sqliteOpen = func(string) (dbw.Dialector, error) {
|
||||
return nil, fmt.Errorf("sqlite is not supported on this platform")
|
||||
}
|
||||
|
||||
type DbType int
|
||||
|
||||
const (
|
||||
UnknownDB DbType = 0
|
||||
Postgres DbType = 1
|
||||
Sqlite DbType = 2
|
||||
)
|
||||
|
||||
func (db DbType) String() string {
|
||||
return [...]string{
|
||||
"unknown",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
}[db]
|
||||
}
|
||||
|
||||
@@ -40,6 +49,8 @@ func StringToDbType(dialect string) (DbType, error) {
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
return Postgres, nil
|
||||
case "sqlite":
|
||||
return Sqlite, nil
|
||||
default:
|
||||
return UnknownDB, fmt.Errorf("%s is an unknown dialect", dialect)
|
||||
}
|
||||
@@ -148,6 +159,12 @@ func Open(ctx context.Context, dbType DbType, connectionUrl string, opt ...Optio
|
||||
DSN: connectionUrl,
|
||||
},
|
||||
)
|
||||
case Sqlite:
|
||||
var err error
|
||||
dialect, err = sqliteOpen(connectionUrl)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to open %s database type", dbType)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sqliteOpen = supportedSqlite
|
||||
}
|
||||
|
||||
func supportedSqlite(s string) (dbw.Dialector, error) {
|
||||
return sqlite.Open(s), nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sqliteOpen = supportedSqlite
|
||||
}
|
||||
|
||||
func supportedSqlite(s string) (dbw.Dialector, error) {
|
||||
return sqlite.Open(s), nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sqliteOpen = supportedSqlite
|
||||
}
|
||||
|
||||
func supportedSqlite(s string) (dbw.Dialector, error) {
|
||||
return sqlite.Open(s), nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sqliteOpen = supportedSqlite
|
||||
}
|
||||
|
||||
func supportedSqlite(s string) (dbw.Dialector, error) {
|
||||
return sqlite.Open(s), nil
|
||||
}
|
||||
Reference in new issue
Block a user