From 11c7c8dbe864e76201ee942d42054474d42e5b60 Mon Sep 17 00:00:00 2001 From: Jeff Malnick Date: Mon, 16 Nov 2020 10:39:18 -0800 Subject: [PATCH] feat: make auth token staleness and duration configurable (#777) * feat: make auth token time to live and staleness configurable * docs: add configuration docs for auth token TTL and staleness Co-authored-by: Jeff Mitchell --- internal/authtoken/options.go | 45 +++++++- internal/authtoken/options_test.go | 18 +++ internal/authtoken/repository.go | 42 +++---- internal/authtoken/repository_test.go | 103 ++++++++++++------ internal/cmd/config/config.go | 30 +++++ internal/servers/controller/controller.go | 4 +- .../content/docs/configuration/controller.mdx | 7 ++ 7 files changed, 188 insertions(+), 61 deletions(-) diff --git a/internal/authtoken/options.go b/internal/authtoken/options.go index a3bdda8236..7683be33e0 100644 --- a/internal/authtoken/options.go +++ b/internal/authtoken/options.go @@ -1,5 +1,16 @@ package authtoken +import ( + "time" + + "github.com/hashicorp/boundary/internal/db" +) + +var ( + defaultTokenTimeToLiveDuration = 7 * 24 * time.Hour + defaultTokenTimeToStaleDuration = 24 * time.Hour +) + // getOpts - iterate the inbound Options and return a struct func getOpts(opt ...Option) options { opts := getDefaultOptions() @@ -14,12 +25,18 @@ type Option func(*options) // options = how options are represented type options struct { - withTokenValue bool - withLimit int + withTokenValue bool + withTokenTimeToLiveDuration time.Duration + withTokenTimeToStaleDuration time.Duration + withLimit int } func getDefaultOptions() options { - return options{} + return options{ + withLimit: db.DefaultLimit, + withTokenTimeToLiveDuration: defaultTokenTimeToLiveDuration, + withTokenTimeToStaleDuration: defaultTokenTimeToStaleDuration, + } } // withTokenValue allows the auth token value to be included in the lookup response. @@ -30,11 +47,31 @@ func withTokenValue() Option { } } +// WithTokenTimeToLiveDuration allows setting the auth token time-to-live. +func WithTokenTimeToLiveDuration(ttl time.Duration) Option { + return func(o *options) { + if ttl > 0 { + o.withTokenTimeToLiveDuration = ttl + } + } +} + +// WithTokenTimeToStaleDuration allows setting the auth token staleness duration. +func WithTokenTimeToStaleDuration(dur time.Duration) Option { + return func(o *options) { + if dur > 0 { + o.withTokenTimeToStaleDuration = dur + } + } +} + // WithLimit provides an option to provide a limit. Intentionally allowing // negative integers. If WithLimit < 0, then unlimited results are returned. // If WithLimit == 0, then default limits are used for results. func WithLimit(limit int) Option { return func(o *options) { - o.withLimit = limit + if limit > 0 { + o.withLimit = limit + } } } diff --git a/internal/authtoken/options_test.go b/internal/authtoken/options_test.go index 1a6e8db240..f628d7aac2 100644 --- a/internal/authtoken/options_test.go +++ b/internal/authtoken/options_test.go @@ -2,6 +2,7 @@ package authtoken import ( "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -9,6 +10,7 @@ import ( // Test_GetOpts provides unit tests for GetOpts and all the options func Test_GetOpts(t *testing.T) { t.Parallel() + t.Run("withTokenValue", func(t *testing.T) { assert := assert.New(t) opts := getOpts(withTokenValue()) @@ -16,4 +18,20 @@ func Test_GetOpts(t *testing.T) { testOpts.withTokenValue = true assert.Equal(opts, testOpts) }) + + t.Run("WithTokenTimeToLiveDuration", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithTokenTimeToLiveDuration(1 * time.Hour)) + testOpts := getDefaultOptions() + testOpts.withTokenTimeToLiveDuration = 1 * time.Hour + assert.Equal(opts, testOpts) + }) + + t.Run("WithTokenTimeToLiveStale", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithTokenTimeToStaleDuration(1 * time.Hour)) + testOpts := getDefaultOptions() + testOpts.withTokenTimeToStaleDuration = 1 * time.Hour + assert.Equal(opts, testOpts) + }) } diff --git a/internal/authtoken/repository.go b/internal/authtoken/repository.go index a88f451eef..b552523450 100644 --- a/internal/authtoken/repository.go +++ b/internal/authtoken/repository.go @@ -14,22 +14,20 @@ import ( "github.com/hashicorp/boundary/internal/kms" ) -// TODO (ICU-406): Make these fields configurable. var ( lastAccessedUpdateDuration = 10 * time.Minute - maxStaleness = 24 * time.Hour - maxTokenDuration = 7 * 24 * time.Hour timeSkew = time.Duration(0) ) // A Repository stores and retrieves the persistent types in the authtoken // package. It is not safe to use a repository concurrently. type Repository struct { - reader db.Reader - writer db.Writer - kms *kms.Kms - // defaultLimit provides a default for limiting the number of results returned from the repo - defaultLimit int + reader db.Reader + writer db.Writer + kms *kms.Kms + limit int + timeToLiveDuration time.Duration + timeToStaleDuration time.Duration } // NewRepository creates a new Repository. The returned repository is not safe for concurrent go @@ -45,15 +43,14 @@ func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repo } opts := getOpts(opt...) - if opts.withLimit == 0 { - // zero signals the boundary defaults should be used. - opts.withLimit = db.DefaultLimit - } + return &Repository{ - reader: r, - writer: w, - kms: kms, - defaultLimit: opts.withLimit, + reader: r, + writer: w, + kms: kms, + limit: opts.withLimit, + timeToLiveDuration: opts.withTokenTimeToLiveDuration, + timeToStaleDuration: opts.withTokenTimeToStaleDuration, }, nil } @@ -91,10 +88,9 @@ func (r *Repository) CreateAuthToken(ctx context.Context, withIamUser *iam.User, return nil, fmt.Errorf("create: unable to get database wrapper: %w", err) } - // TODO: Allow the caller to specify something different than the default duration. // We truncate the expiration time to the nearest second to make testing in different platforms with // different time resolutions easier. - expiration, err := ptypes.TimestampProto(time.Now().Add(maxTokenDuration).Truncate(time.Second)) + expiration, err := ptypes.TimestampProto(time.Now().Add(r.timeToLiveDuration).Truncate(time.Second)) if err != nil { return nil, err } @@ -211,7 +207,7 @@ func (r *Repository) ValidateToken(ctx context.Context, id, token string, opt .. sinceLastAccessed := now.Sub(lastAccessed) + timeSkew // TODO (jimlambrt 9/2020) - investigate the need for the timeSkew and see // if it can be eliminated. - if now.After(exp.Add(-timeSkew)) || sinceLastAccessed >= maxStaleness { + if now.After(exp.Add(-timeSkew)) || sinceLastAccessed >= r.timeToStaleDuration { // If the token has expired or has become too stale, delete it from the DB. _, err = r.writer.DoTx( ctx, @@ -277,13 +273,9 @@ func (r *Repository) ListAuthTokens(ctx context.Context, withOrgId string, opt . return nil, fmt.Errorf("list users: missing org id %w", errors.ErrInvalidParameter) } opts := getOpts(opt...) - limit := r.defaultLimit - if opts.withLimit != 0 { - // non-zero signals an override of the default limit for the repo. - limit = opts.withLimit - } + var authTokens []*AuthToken - if err := r.reader.SearchWhere(ctx, &authTokens, "auth_account_id in (select public_id from auth_account where scope_id = ?)", []interface{}{withOrgId}, db.WithLimit(limit)); err != nil { + if err := r.reader.SearchWhere(ctx, &authTokens, "auth_account_id in (select public_id from auth_account where scope_id = ?)", []interface{}{withOrgId}, db.WithLimit(opts.withLimit)); err != nil { return nil, fmt.Errorf("list users: %w", err) } for _, at := range authTokens { diff --git a/internal/authtoken/repository_test.go b/internal/authtoken/repository_test.go index 68a264f421..34fc95d91b 100644 --- a/internal/authtoken/repository_test.go +++ b/internal/authtoken/repository_test.go @@ -42,33 +42,79 @@ func TestRepository_New(t *testing.T) { }{ { name: "valid default limit", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + opts: []Option{}, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + limit: db.DefaultLimit, + timeToLiveDuration: defaultTokenTimeToLiveDuration, + timeToStaleDuration: defaultTokenTimeToStaleDuration, + }, + }, + { + name: "valid new limit", args: args{ r: rw, w: rw, kms: kmsCache, + opts: []Option{ + WithLimit(5), + }, }, want: &Repository{ - reader: rw, - writer: rw, - kms: kmsCache, - defaultLimit: db.DefaultLimit, + reader: rw, + writer: rw, + kms: kmsCache, + limit: 5, + timeToLiveDuration: defaultTokenTimeToLiveDuration, + timeToStaleDuration: defaultTokenTimeToStaleDuration, }, }, { - name: "valid new limit", + name: "valid token time to live", args: args{ - r: rw, - w: rw, - kms: kmsCache, - opts: []Option{WithLimit(5)}, + r: rw, + w: rw, + kms: kmsCache, + opts: []Option{ + WithTokenTimeToLiveDuration(1 * time.Hour), + }, }, want: &Repository{ - reader: rw, - writer: rw, - kms: kmsCache, - defaultLimit: 5, + reader: rw, + writer: rw, + kms: kmsCache, + limit: db.DefaultLimit, + timeToLiveDuration: 1 * time.Hour, + timeToStaleDuration: defaultTokenTimeToStaleDuration, }, }, + { + name: "valid token time to stale", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + opts: []Option{ + WithTokenTimeToStaleDuration(1 * time.Hour), + }, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + limit: db.DefaultLimit, + timeToStaleDuration: 1 * time.Hour, + timeToLiveDuration: defaultTokenTimeToLiveDuration, + }, + }, + { name: "nil-reader", args: args{ @@ -302,6 +348,7 @@ func TestRepository_ValidateToken(t *testing.T) { kms := kms.TestKms(t, conn, wrapper) iamRepo := iam.TestRepo(t, conn, wrapper) repo, err := NewRepository(rw, rw, kms) + require.NoError(t, err) require.NotNil(t, repo) @@ -417,9 +464,6 @@ func TestRepository_ValidateToken_expired(t *testing.T) { wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) iamRepo := iam.TestRepo(t, conn, wrapper) - repo, err := NewRepository(rw, rw, kms) - require.NoError(t, err) - require.NotNil(t, repo) org, _ := iam.TestScopes(t, iamRepo) baseAT := TestAuthToken(t, conn, kms, org.GetPublicId()) @@ -431,9 +475,6 @@ func TestRepository_ValidateToken_expired(t *testing.T) { require.NoError(t, err) require.NotNil(t, iamUser) - defaultStaleTime := maxStaleness - defaultExpireDuration := maxTokenDuration - var tests = []struct { name string staleDuration time.Duration @@ -442,20 +483,20 @@ func TestRepository_ValidateToken_expired(t *testing.T) { }{ { name: "not-stale-or-expired", - staleDuration: maxStaleness, - expirationDuration: maxTokenDuration, + staleDuration: defaultTokenTimeToStaleDuration, + expirationDuration: defaultTokenTimeToLiveDuration, wantReturned: true, }, { name: "stale", - staleDuration: 0, - expirationDuration: maxTokenDuration, + staleDuration: 1 * time.Millisecond, + expirationDuration: defaultTokenTimeToLiveDuration, wantReturned: false, }, { name: "expired", - staleDuration: maxStaleness, - expirationDuration: 0, + staleDuration: defaultTokenTimeToStaleDuration, + expirationDuration: 1 * time.Millisecond, wantReturned: false, }, } @@ -464,10 +505,14 @@ func TestRepository_ValidateToken_expired(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - maxStaleness = tt.staleDuration - maxTokenDuration = tt.expirationDuration timeSkew = 20 * time.Millisecond + repo, err := NewRepository(rw, rw, kms, + WithTokenTimeToLiveDuration(tt.expirationDuration), + WithTokenTimeToStaleDuration(tt.staleDuration)) + require.NoError(err) + require.NotNil(repo) + ctx := context.Background() at, err := repo.CreateAuthToken(ctx, iamUser, baseAT.GetAuthAccountId()) require.NoError(err) @@ -482,10 +527,6 @@ func TestRepository_ValidateToken_expired(t *testing.T) { assert.Error(db.TestVerifyOplog(t, rw, at.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE))) assert.Nil(got) } - - // reset the system default params - maxStaleness = defaultStaleTime - maxTokenDuration = defaultExpireDuration }) } } diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 9e19e21d8e..574cfae746 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -11,10 +11,12 @@ import ( "net/url" "os" "strings" + "time" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/hcl" "github.com/hashicorp/shared-secure-libs/configutil" + "github.com/hashicorp/vault/sdk/helper/parseutil" ) const ( @@ -99,6 +101,15 @@ type Controller struct { Description string `hcl:"description"` Database *Database `hcl:"database"` PublicClusterAddr string `hcl:"public_cluster_addr"` + + // AuthTokenTimeToLive is the total valid lifetime of a token denoted by time.Duration + AuthTokenTimeToLive interface{} `hcl:"auth_token_time_to_live"` + AuthTokenTimeToLiveDuration time.Duration + + // AuthTokenTimeToStale is the total time a token can go unused before becoming invalid + // denoted by time.Duration + AuthTokenTimeToStale interface{} `hcl:"auth_token_time_to_stale"` + AuthTokenTimeToStaleDuration time.Duration } type Worker struct { @@ -211,6 +222,25 @@ func Parse(d string) (*Config, error) { return nil, err } + // Perform controller configuration overrides for auth token settings + if result.Controller != nil { + if result.Controller.AuthTokenTimeToLive != "" { + t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToLive) + if err != nil { + return result, err + } + result.Controller.AuthTokenTimeToLiveDuration = t + } + + if result.Controller.AuthTokenTimeToStale != "" { + t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToStale) + if err != nil { + return result, err + } + result.Controller.AuthTokenTimeToStaleDuration = t + } + } + sharedConfig, err := configutil.ParseConfig(d) if err != nil { return nil, err diff --git a/internal/servers/controller/controller.go b/internal/servers/controller/controller.go index 116d69ccf7..fde393ce26 100644 --- a/internal/servers/controller/controller.go +++ b/internal/servers/controller/controller.go @@ -112,7 +112,9 @@ func New(conf *Config) (*Controller, error) { return static.NewRepository(dbase, dbase, c.kms) } c.AuthTokenRepoFn = func() (*authtoken.Repository, error) { - return authtoken.NewRepository(dbase, dbase, c.kms) + return authtoken.NewRepository(dbase, dbase, c.kms, + authtoken.WithTokenTimeToLiveDuration(c.conf.RawConfig.Controller.AuthTokenTimeToLiveDuration), + authtoken.WithTokenTimeToStaleDuration(c.conf.RawConfig.Controller.AuthTokenTimeToStaleDuration)) } c.ServersRepoFn = func() (*servers.Repository, error) { return servers.NewRepository(dbase, dbase, c.kms) diff --git a/website/content/docs/configuration/controller.mdx b/website/content/docs/configuration/controller.mdx index a8a7a3adc2..cc05d32531 100644 --- a/website/content/docs/configuration/controller.mdx +++ b/website/content/docs/configuration/controller.mdx @@ -40,6 +40,13 @@ used by workers after initial connection to controllers via the worker's bind a publicly accessible IP to a NIC on the host directly, such as an Amazon EIP. +- `auth_token_time_to_live` - Maximum time to live (TTL) for all auth tokens globally (pertains +to all tokens from all auth methods). Valid time units are anything specified by Golang's +[ParseDuration()](https://golang.org/pkg/time/#ParseDuration) method. Default is 7 days. +- `auth_token_time_to_stale` - Maximum time of inactivity for all auth tokens globally (pertains +to all tokens from all auth methods). Valid time units are anything specified by Golang's +[ParseDuration()](https://golang.org/pkg/time/#ParseDuration) method. Default is 1 day. + ## KMS Configuration The controller requires two KMS stanzas for `root` and `worker-auth` purposes: